【人人都能看懂的漫画算法】边打扑克边学插入排序算法,彻底搞懂时间复杂度

本文主要是介绍【人人都能看懂的漫画算法】边打扑克边学插入排序算法,彻底搞懂时间复杂度,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

博主:爱码叔
个人博客站点: icodebook.com
公众号:爱码叔漫画软件设计(搜:爱码叔)
微博:程序员涛哥
专注于软件设计与架构、技术管理。擅长用通俗易懂的语言讲解技术。对技术管理工作有自己的一定见解。文章会第一时间首发在个站上,欢迎大家关注访问!

写在前面的话

今天我开了一个新的专栏,关于算法。算法的教材和文章相当的多,为什么我还要做这个专栏呢?

首先算法比较复杂,复杂在思想上,复杂在大量数学知识的运用上。因此市面上有些非常优秀的书,讲的很细,内容很全,但是门槛比较高。还有些算法书非常薄,但只讲了部分算法的思想,给出了实现代码而已。甚至连时间复杂度的 “O” 标记都没有解释。又让我觉得过于精简了。于是我萌生了写自己心目中算法应该怎么讲的文章。

再说说本专栏的文章形式。大概一年半前,我第一次尝试用漫画的形式创作了几篇关于安全的文章。
《图文结合彻底理解非对称加密、RSA原理及安全性》

《加密就像玩魔方----图文详解对称加密》

《图文轻松理解计算机网络五层架构》

反响还不错。于是我想尝试挑战用漫画的形式创作算法专栏。

虽然工作中很少使用算法,但却是程序员的必须课。希望看我的算法专栏能让读者更为轻松的学习到算法知识!

专栏的内容会参考《算法导论》、《算法》等经典著作,将里面经典的内容以通俗易懂的形式,并辅以大量插图呈现给大家。

文章中有两位虚拟人物:

  • 经验丰富的老司机程序员----熊小猫
  • 初入职场激情满满的菜鸟程序员----兔小白

让我们跟随兔小白和熊小猫一起走入算法的世界!

下面开始第一篇正文


边打扑克边学插入排序算法,彻底搞懂时间复杂度

什么是算法、什么是时间复杂度、时间复杂度中的O标记是什么意思、如何分析算法的时间复杂度?咱们跟着兔小白和熊小猫,边打扑克边学!基础知识掌握好,后面学到复杂算法才会更从容!
在这里插入图片描述
熊小猫:晚上放松下,来我家打牌怎么样?

兔小白:不去了,我已经计划从今晚开始学习算法

熊小猫:在游戏中学习效果最好!其实抓牌的过程就存在一个算法!

兔小白:哦!?难道有抓到好牌的算法?

熊小猫:想什么呢!你说的是出老千!

兔小白:嘿嘿…

熊小猫:抓牌时候是不是要按顺序排放你手中的牌?这里就用到了插入排序。

兔小白:啊,之前我面试挂了,考的就是插入排序!没想到我打了20年扑克牌,竟然一直都在实践插入排序!?

熊小猫:今晚打完扑克,包你彻底掌握插入排序!

插入排序如何工作

下班后,兔小白来到了熊小猫家里打扑克。

兔小白:来吧!讲讲插入排序,请开始你的表演!

熊小猫:别着急,我先发牌。发完后,你再摸牌。边摸边思考下摸牌的过程。

兔小白:手里的牌需要从小到大排列,每次我摸牌后都会从小的一侧开始比较,直到找到比我抓到的牌大的那一张,然后插在那一张牌的前面
在这里插入图片描述
熊小猫:没错!这就是插入排序。我们仔细看看这个过程。
在这里插入图片描述
熊小猫:插入排序是不是很简单?你看看能不能用代码实现?

兔小白:等等!我的牌不错,先打完这局!

插入排序代码

熊小猫:你这牌也太好了,我认输了。

兔小白:作为惩罚,再给我讲讲代码实现吧?

熊小猫:没问题!咱先写一种最自然地,符合我们直觉地摸牌算法。首先你从牌堆里抓出一张牌,然后从左向右和手牌进行比较,找到比你抓到的牌大的那一张,你会把这张牌插入到它的前面。如果找到手牌的牌尾还没找到,那么放到牌尾。

兔小白:我想想…没错,确实我抓牌时候非常自然地就是这么做的! 但从你嘴里以这么严谨的逻辑说出来,我还得理解一下,嘿嘿!

熊小猫:那我们就按照这个逻辑写代码。

public static Integer[] sort(Integer[] unsorted) {//sorted数组可以理解为拿着排好序手牌的"手"Integer[] sorted = new Integer[unsorted.length];//第一张牌什么都不用想,放入手中sorted[0] = unsorted[0];//从第二张牌开始要逐张找到其顺序的位置放入for (int i = 1; i < unsorted.length; i++) {//摸起一张牌,从左侧开始比较,找到插入的位置。最差的情况要找到手牌的牌尾for (int j = 0; j <= i; j++) {//如果已经找到手牌牌尾或者中途找到手牌中比当前牌大的那张牌if (j==i || unsorted[i] < sorted[j]) {//比当前大的那张牌以及后面的牌向后移一位for (int k = i-1; k >= j; k--) {sorted[k + 1] = sorted[k];}//将当前牌放入移动后空出来的那个位置。sorted[j] = unsorted[i];//本轮结束break;}}}return sorted;
}

兔小白:注释都有了!但我还是看不太懂。

熊小猫:其实很好理解,咱们跟着注释一行行的看。

  1. 函数的输入是一个无序的整型数组 unsorted,可以理解为桌面牌堆
  2. 第3行初始化的整型数组 sorted,可以理解为你的“手”,用来放排好序的牌。这里记住,手中牌是 sorted 中放的牌,桌面牌堆是 unsorted 中放的牌。
  3. 第5行代码是你抓出第一张牌时,手里还是空的。所以直接放入你的手里,也就是 sorted 的0号位置。
  4. 第7行开始进入主要的算法逻辑。从第二张牌开始,逐一找到自己的位置放进来。直到抓完所有的牌,也就是循环完 unsorted。
    1. 摸起一张牌 unsort[i],然后从左向右,和手中牌比较大小。摸起的是第 i 张牌时,你手中有 i-1 张牌。但最差的情况要比较 i 次,因为如果很不幸,你拿到的牌比手里的 i-1 牌都大,要再比较一次以判断是否走到现有手牌的牌尾。(第9行代码)
    2. 第 11 行代码,就是在比较摸起的牌和手里的牌,目的是找到比摸起的牌大的牌的位置 j。条件语句中的 j==i 表示找到手牌牌尾也没有找到比摸到的牌更大的牌。
    3. 13-15 行这个循环体做的事情是,将 sorted 中 j 位置后面的牌往后移一位,空出 j 这个位置,用来放摸到的牌。
    4. 17 行代码便是将抓的牌 放入手中正确的位置 sorted[j]
    5. 19 行的 break 表示一轮插入逻辑结束,也就是第 9 行的 for 循环结束。此时已经将这次抓到的牌放入手牌中正确位置。

如此往复 4.1~4.5,直到所有 unsorted 中的元素都有序的放入 sorted。

兔小白:代码逻辑和我抓牌时脑子里想的一模一样!!我脑子只是转了一下,代码表达出来居然写了这么多?

熊小猫:哈哈,别看代码多,但计算机执行起来可比你脑子快的多!

兔小白:我突然想到有时候我会先把牌堆的牌全拿到手中再排顺序。这样排序会不会更快?

熊小猫:这个问题的本质是如何评价算法,解决问题的办法不止一种,如何评价哪种更好呢?通常我们使用算法复杂度来评价算法。

兔小白: 算法复杂度?说来听听?

熊小猫:别急,上面我给出的算法并不是最经典的插入排序。我再送你一份经典的插入排序代码,加量不加价!这个算法就是你说的把牌全部拿到手中再排序,这样就只需一个数组,节省了存储空间。
算法的主要逻辑是,从第二个元素开始,和前面的元素逐一比较,如果自己较小,就和前面的交换位置,然后再继续和前面的比较。直到找到前面比自己还小的那个元素,就停在当前的位置,然后开始下一轮循环。

代码如下,你先自己研究下,看明白后加上注释。

public static Integer[] sortV2(Integer[] unsorted) {for (int i = 1; i < unsorted.length; i++) {for (int j = i; j > 0 && unsorted[j] < unsorted[j - 1]; j--) {Integer toBeSortedNumber = unsorted[j];unsorted[j] = unsorted[j - 1];unsorted[j - 1] = toBeSortedNumber;}}return unsorted;
}

兔小白:考我是吧?就这几行代码,五分钟搞定!

五分钟后兔小白给代码加上了注释

public static Integer[] sortV2(Integer[] unsorted) {//从第二个元素开始,向后逐一执行排序逻辑for (int i = 1; i < unsorted.length; i++) {//从待排序元素的位置开始,向前逐一比较。比自己大就交换位置,比自己小停止循环。for (int j = i; j > 0 && unsorted[j] < unsorted[j - 1]; j--) {//如果前一个元素比当前元素大,那么交换两个元素的位置Integer toBeSortedNumber = unsorted[j];unsorted[j] = unsorted[j - 1];unsorted[j - 1] = toBeSortedNumber;}}return unsorted;
}

熊小猫:这个代码是不是简洁多了。下面咱们再聊聊算法的复杂度。

如何评价算法?

兔小白:我经常听别人讨论算法复杂度。什么是算法复杂度?是算法代码的行数吗?

熊小猫:代码行数?你别闹。。。通常算法复杂度包括时间复杂度和空间复杂度。下面我慢慢解释。
算法用来解决问题。生活中也有各种各样的问题要解决。比如我从北京开车去北戴河应该怎么走。线路的选择有很多。有的路程虽然远,但是车少、路好走,所以时间短。有的路程虽然近很多,但是车多拥堵,导致时间反而更长。
在这里插入图片描述
看完这个例子,如何评判算法优劣,相信我们心里已经有数。

首先是算法的速度,也就是解决问题所消耗的时间。这就是我们常说的“时间复杂度”

然后是算法消耗的资源,例子中是公路,资源是要付出代价的,体现在油费和高速费。对于算法来说,运算中消耗的资源是计算机的存储。计算过程中使用的存储数量,就是 “空间复杂度”。

你一定听说过“以时间换空间”或者“以空间换时间”。算法中的也有这个概念,牺牲更大的存储空间换取更短的运算时间,或是以更长的运算时间换取更小的存储使用。

究竟是时间优先还是空间优先,这取决于你手里的资源。如果你想更快到达北戴河,甚至可以乘坐直升飞机,但是成本飙升。我们永远渴望更快的运算速度,但是又被成本所牵制。不过在这个存储非常廉价的时代,可以更多考虑运算时间。

除了时间复杂度和空间复杂度外,还可以从以下几个方面评价算法的优劣。

  • 易读性。代码是否简洁、易读,便于维护。从工程角度看,这一点非常重要。
  • 健壮性。对于非法输入是否有足够的包容性。
  • 安全性。算法是否有安全漏洞,可以被利用、攻击。尤其是安全相关的算法。

这几点主要在工程实践、非功能性需求等方面对算法提出近一步要求。
我们回归到算法本身,算法的评判标准是时间复杂度空间复杂度

时间复杂度

熊小猫:正如汽车的性能不一样,计算机的性能也不一样。对于时间的度量,需要在统一标准的机器上进行。我们假设这台机器基本的指令(算术、移动、控制)都能在常量时间内运行完成。

算法运行的时间一般和输入规模同步增长。10牌排序肯定快于50张牌。因此,一般将运算时间用输入规模的函数表达。

咱们看个很简单的例子。还是打扑克,我现在出 K,你需要出比K大的牌管上我。假设这个扑克牌玩法只能单张出牌,每人手里10张牌(虽然很愚蠢,但能简化理解),并且已经排好序。这里给出一个最直观地出牌算法:从第一张开始逐张进行比较,如果小于等于 K,继续比较下一张,直到找到大于 K 的牌打出此牌。如果找到牌尾没有找到,则告知对方不出牌。

假设你手里有n张牌,那么输入规模就是n。每次比较所消耗的时间是 t1,找到牌后选择出牌或者告知对方不出牌消耗时间t2。我们考虑最差的情况,会比较完手里的所有牌。此时消耗的时间是n*t1,算法的时间函数如下。

T(n) = t1*n + t2

这是一个非常精准的时间计算。但在计算机领域,我们更关心的是运行时间随 n 的增长率或者增长量级。因此可以忽略掉常数项、低阶项、常数系数。我们可以看个例子感受一下。

假设我们有另外一个出牌算法,时间函数为

T’(n) = c1*n^2+c2

即使 t1 比 c1 大很多,t2 也比 c2 大很多,也终究无法改变一个事实:当 n 足够大时,T‘(n) 将永远超越T(n)。通过下面这个示意图可以直观的感受这个结论。
在这里插入图片描述
可以看出常数项和常数系数并不重要。因此,我们比较算法的时间复杂度时,通常将常数项以及常数系数忽略掉。但不可以将 T(n) = t1*n + t2 转化为 T(n) = n 。正确的写法是 T(n) = O(n)

兔小白:我理解函数的最高阶项决定了他的增长量级,低介项和常量系数可以忽略。

熊小猫:没错,理解的很到位!

兔小白:但是这个 “大欧” 是什么符号?我确实见过 T(n) = O(n) 这种写法,不过一直不理解。

熊小猫:别急,接下来我们就看看记号 “大欧”。

O记号

熊小猫:“大欧” 用来标记一个函数的渐进上界。前面讲过算法时间函数的增长量级可以用来比较算法的时间复杂度。那么渐进上界和增长量级是一回事吗?我们先来看看什么是渐进上界。
看下图的两个函数 f(n)=t1*n + t2 及 g(n)=n。

在这里插入图片描述
当我们将g(n)乘以一个大于1的系数c,得到一个新的函数cg(n)。
在这里插入图片描述
可以看到在相交点 n0 后,对于所有 n ,都有 cg(n)>f(n)。这就是渐进上界的定义。对于给定的函数 g(n), 如果存在正常量 c和n0 使得 对所有 n>n0, 有 0<=f(n)<=cg(n),那么 g(n) 就是 f(n) 的渐进上界。 记作 f(n)=O(g(n))。

单看文字定义,你一定云里雾里。我们来看上面的图,当g(n)乘以一个大于0的系数c后,cg(n)的线会比f(n)更为陡峭,这意味着在某个点n0,cg(n)的线将永远在f(n)的上方。

兔小白:等等,这里我有个问题,根据渐进确界的定义,t1*n + t2=O(n^2) 也是成立的,你看下面我画的图,完全符合定义!
在这里插入图片描述
但是 t1 * n^2+ t2 的渐进上界也是 O(n^2) 。那么渐进上界就无法用来比较时间复杂度。

熊小猫:这个问题非常非常好!渐进上界并不能准确描述一个函数的增长量级。其实很多算法书中将O用作标记渐进紧确界(标准的标记应该用Θ)。渐进紧确界除了满足渐进上界的要求之外,还需要存在c‘使得所有 n>n0, 有cg(n)>f(n)>c’g(n)。 文字太抽象,还是看图吧。
在这里插入图片描述
函数 cn^2,即使c再小,终究还是会高过 t1n + t2 。所以 g(n)=n^2 不满足对 f(n)= t1*n + t2 的渐进紧确界的定义。但函数 g(n)=n 满足定义,因此 n=O(n) 成立,但 n=O(n^2) 是错误的。

兔小白:明白了,我们通常看的算法书中把O用作标记渐进紧确界而不是渐进上界。

熊小猫:没错,函数的渐进紧确界可以代表它的增长趋势。因为通过调整 g(n) 的系数 c 只能另函数曲线更陡峭或更平缓,但不会改变增长趋势。也就是说不会改变匀速增长、加速增长、减速增张的大趋势。
如果当n无穷大时,f(n)仍不能越界出 cg(n) 和 c‘g(n),而是被死死的夹住了。那么可以认为g(n)代表了f(n)的增长趋势,这就是为什么我们可以用T(n)的渐进紧确界来表示算法的时间复杂度。

兔小白:说实话数学这块把我搞得有点晕。虽然勉强能听懂,但用的时候恐怕又忘了。有没有简单的办法快速得到函数的渐进紧确界。

熊小猫:哈哈,这个其实最开始就说过了,通常来说,你把函数的常数项、低介项、常数系数去掉就可以了。

兔小白:这么简单?你看看我这几个算的对不对?

  1. an^2 + bn + c=O(n^2)
  2. nlgn+an+c = O(lgn)

熊小猫:第一个没问题,第二个错啦!应该是 O(nlgn)。n 并不是常数系数,需要保留。

兔小白:大意了!咱们说了半天还没说插入算法的时间复杂度呢。

熊小猫:别急,干任何事情都要打好基础,咱们这就看看插入算法的时间复杂度。

插入排序时间复杂度

熊小猫:基础打好了,我们来看看插入排序的时间复杂度。先问个问题,摸牌这个阶段的时间消耗每次都一样吗?

兔小白:当然不一样,有时候我着急就会摸的快一点!
在这里插入图片描述
熊小猫:不是这个意思…我是说每次找到插入位置所需的比较次数是固定的吗?

兔小白:当然不是,如果运气好,每次都比第一张牌小,那么每次都只需要比较一次。但是运气不好摸出一张比手里牌都大的牌,那么就要比较所有手里的牌,最后放到牌尾。

熊小猫:没错!如果牌堆里的牌是逆顺序排好,最大的牌在最上面,那么每次摸牌我都只需要比较一次。反之如果牌堆里的牌是顺序排好,最小的在最上面,那么每次摸牌都要比较手里所有的牌。

兔小白:碰到这种情况,我很快就会发现规律,我会改为从手中牌的右侧开始比。还是非常快!

熊小猫:你这已经改变算法啦!如果给算法加上智能,也不是不行,今天咱们不讨论这个。

我们讨论算法的时间复杂度,一般是看的最坏情况。也就是算法最长的运行时间。这和你评估工作量一样,3-5天的工作量,你肯定报5天。

兔小白:别提了,工作量我就没估准过,每次都是加班才搞定。

熊小猫:我们先计算算法消耗的总时间。算法中每个操作消耗的时间算然不同,但为常量。我们再分析出每个操作步骤执行的次数。两者相乘,得到一个步骤的总时长。再把所有操作步骤的消耗加起来就能得到运行时间的函数。另外由于消耗的时间是常量,根据渐进紧确界的特性,常量和常数系数是可以忽略的,那么我们只需要关心总的执行次数即可。
我们来分析第一版算法代码的运算次数。

public static Integer[] sort(Integer[] unsorted) {Integer[] sorted = new Integer[unsorted.length];              sorted[0] = unsorted[0];                                      for (int i = 1; i < unsorted.length; i++) {for (int j = 0; j <= i; j++) {if (j==i || unsorted[i] < sorted[j]) {for (int k = i-1; k >= j; k--) {sorted[k + 1] = sorted[k];}sorted[j] = unsorted[i];break;}}}return sorted;
}

2行:1
3行:1
4行:n
5行 :从i=1到n-1,累加ti,
6行: 从i=1到n-1,累加ti
7行:从i=1到n-1,累加 i-ti+1
8行:从i=1到n-1,累加 i-ti+1
10行:n-1
11 行:n-1

2,3,4 行执行次数很好理解。在第 5 行的 for 循环体中,如果找到了 unsorted[i] 的位置,并放置好后会跳出循环,但查找所用的次数是无法确定的,我们用ti表示。由于要从 i=1 执行到 i=n-1,所以第5,6行代码代码执行的次数为从i=1到n-1,累加ti。

再看 7,8 行。这段代码逻辑是将找到 unsorted[i] 插入位置及之后的元素往后移动一个位置。找到unsorted[i] 插入的位置我们用了 ti 次比较。插入位置为 ti-1。此时手中牌最后一张牌的位置为 i-1。那么需要移动的牌的数量为 i-1-(ti-1)+1,即为 i-ti+1。
在这里插入图片描述
由于数组下标从0,而不是1开始,所以会出现+1或-1,做成一些理解上的困难。其实你只要理解原理:移动的次数=当前手里牌的数量-(比较的次数-1)。

而且之前也提到过,时间复杂度其实只和高阶项有关系,所以这里只要不出现阶的误差就不会影响大局。

根据分析我们列出时间函数。
在这里插入图片描述
熊小猫:根据我们前面讲的内容,你来给出T(n)的渐进紧确界?

兔小白:这个简单,去掉常数项-2和低阶项4n,T(n)=O(n^2)。

熊小猫:可以啊,今晚扑克没白打!

兔小白:嗨,今晚光学习了,哪打扑克了。不过收获确实非常大!终于可以睡觉了!已经快12点了

熊小猫:别着急睡觉,你忘了还有经典的插入排序算法没有分析呢?

兔小白:让我来!我今天要看看凌晨四点半的北京!

熊小猫:你分析着,我先去睡觉了。明天还得上班呢。

public static Integer[] sortV2(Integer[] unsorted) {for (int i = 1; i < unsorted.length; i++) {for (int j = i; j > 0 && unsorted[j] < unsorted[j - 1]; j--) {Integer toBeSortedNumber = unsorted[j];unsorted[j] = unsorted[j - 1];unsorted[j - 1] = toBeSortedNumber;}}return unsorted;
}

兔小白:看着不难,争取半小时后睡觉!

一小时后…

兔小白:时间复杂度搞定了!

2行: n
3行:从 i=1 到 n-1,累加ti,
4,5,6行:均为从 i=1 到 n-1,累加 ti-1,

由于循环体跳出需要多判断一次,所以假如第三行是 ti 次,那么 4,5,6 行 为 ti-1。时间函数如下:
在这里插入图片描述
ti 的取值不确定。最好的情况是牌的顺序已经排好,每次只需要比较1次,并且不需要交换元素位置,ti都是1。时间复杂度如下:

T(n) = n + 1*(n-1)=2n-1=O(n)

最差的情况牌为倒序,每一轮都要和排好序的牌全部交换一次才结束。所以交换的次数为i,比较的次数多一次为i+1。我们把 ti=i+1 带入计算。
在这里插入图片描述
这种写法不但在最好的情况下时间复杂度是线性的,而且代码简洁,还省内存。看来写出正确的算法并不难,难的是写出好算法!终于可以踏实睡觉了!

这篇关于【人人都能看懂的漫画算法】边打扑克边学插入排序算法,彻底搞懂时间复杂度的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/244155

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

poj 3974 and hdu 3068 最长回文串的O(n)解法(Manacher算法)

求一段字符串中的最长回文串。 因为数据量比较大,用原来的O(n^2)会爆。 小白上的O(n^2)解法代码:TLE啦~ #include<stdio.h>#include<string.h>const int Maxn = 1000000;char s[Maxn];int main(){char e[] = {"END"};while(scanf("%s", s) != EO

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

dp算法练习题【8】

不同二叉搜索树 96. 不同的二叉搜索树 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。 示例 1: 输入:n = 3输出:5 示例 2: 输入:n = 1输出:1 class Solution {public int numTrees(int n) {int[] dp = new int

Codeforces Round #240 (Div. 2) E分治算法探究1

Codeforces Round #240 (Div. 2) E  http://codeforces.com/contest/415/problem/E 2^n个数,每次操作将其分成2^q份,对于每一份内部的数进行翻转(逆序),每次操作完后输出操作后新序列的逆序对数。 图一:  划分子问题。 图二: 分而治之,=>  合并 。 图三: 回溯: