排序算法入门之递归思想,循环不变量,归并排序算法与快速排序算法

本文是学习算法不好玩的学习笔记,配合视频链接学习更好。视频链接如下:https://space.bilibili.com/236935093

算法入门之排序算法(算法不好玩)

递归

递归是函数自己调用自己来解决问题的一种方式。它体现的是一种自顶向下的解决问题的思路。、

递归解决问题有两个过程:

  1. 第一个过程是拆分问题的过程

  2. 第二个过程是组合问题的解的过程

    image-20221128183313201

这两个过程是独立的,即递归解决问题时是先拆分到不可以再拆分(拆到问题的边界)之后才开始组合问题的解。而只有由问题边界一步步推导出原问题的解的过程叫做递推。递归是先自顶向下再自底向上,递推只有自底向上。

image-20221128183612792

在计算机中实现递归的数据结构叫做栈,因为它是一种支持后进先出应用场景的线性的数据结构。它可以保证我们拆分问题过程中后拆分的问题先计算出结果。

如果原问题和子问题之间是线性结构那么直接进行拆分然后组合就可以了,但是如果它们之间是树形结构,那么其是按照深度优先遍历的方式进行拆分和组合的(即递归),在遍历时,程序需要记住哪些节点还没有访问过,这一操作就是通过栈来实现的(没访问的节点先放入栈中然后访问其他的节点,利用的就是后进先出的特性,这样我们访问后面的节点不会影响前面的节点,等到其需要访问时再取出来)。

image-20221128185221580

递归总结:

image-20221128185453299

image-20221128185525350

image-20221128185729880

时间复杂度

时间复杂度是一个动态的概念,它表示随着输入规模的增大,程序的运行时间增加的快慢

image-20221128194948993

具体时间复杂度的表示法是使用大O表示法,求法如下:

image-20221128195846040

时间复杂度的极限定义如下:

image-20221128200040771

g(N)就是大O括号中的内容,f(N)就是具体的我们代码的复杂度,我们可以找出一个其上界即cg(N),这里c是一个常数对于时间复杂度来说并不重要。所以g(N)就是我们要求的时间复杂度。

image-20221128200443093

插入排序:

image-20221128202349096

循环不变量:

循环不变量是一个性质:在循环过程中保持不变的性质

这里的量并不是指变量,在循环过程中变量的值虽然会发生变化,但是循环过程中会有一些性质保持不变,这些性质就是循环不变量,这里的量指的是一些断言(可以判断真假的语句),循环不变量有以下性质:

image-20221127190716194

循环开始前表示一个基本的情况,循环过程中说明一个递推的情况,这就推出了循环结束时这个断言依旧成立。而循环结束之后成立的这件事情往往就是我们要求的结论。前两个性质是原因,最后一个性质是结果。

image-20221127191127397

image-20221127191008580

这里保持中应该是使得nums[0…i+1)有序,这样下次开始前就保存了性质。

这两个例子都说明了循环不变量,其实它非常简单,因为我们这两个排序都是一次排定一个元素在数组头部,所以我们很容易就可以分析出来循环不变量。

后面的查找并且移动数组元素和这里排序是一样的,如果是闭区间则表示当前i可以取到,也就是i包含在循环不变量的区间中,那么i表示符合循环不变量的元素,即已经处理过的符合要求的元素,如果是开区间则表示i娶不到,即i是即将要处理的元素的位置。

归并排序:

归并排序是使用的是递归算法,这个算法的思想是分治思想(拆分问题与组合问题的解),递归结构即代码执行的方式为深度优先遍历的方式进行执行的。

image-20221128164812652

深度优先遍历的递归具体执行时代码入栈顺序为先执行的代码后入栈,后执行的代码先入栈,因为栈的结构是后进先出即后来的代码先执行。

快速排序:

快速排序也是利用的分治的思想,使用的递归算法来实现的。最普通的快速排序是从前向后遍历元素,选定一个pivot(切分)元素,以这个元素为界进行划分区间(<或者>=),简单记忆就是六个字:大放过小替换。这种方法的pivot元素取的是数组区间的left下标的元素,这种方法有一个缺点就是数组如果是顺序或者倒序的,那么效率会非常低。

image-20221129180249105

然后我们可以通过随机(Random)选取切分元素来改进这种方法。但是随机选取无法处理如果数组中有很多相同元素时的情况,因为随机选取的很可能是一个在数组中有很多重复元素的元素。因此我们又引进了新的优化算法。

双路快排:

image-20221129181715725

双路快排是区间两个指针(左右指针),然后相向遍历区间,左指针遇到第一个不小于pivot元素的元素时停下,然后开始移动右指针,右指针一直移动到第一个不大于pivot元素的元素时停下,然后交换这两个元素。

在最后退出整个遍历区间的循环时:

image-20221129182254561 image-20221129182323018

因为ge的访问在le之后。

三路快排:

image-20221129185208214

image-20221129185219382

具体操作如下:

image-20221129185326226

三路快排之所以快是因为我们把所有等于pivot元素的元素都挤到了数组的中间,那么下次划分区间时我们就能递归处理更小的数组区间,这样执行效率就会大大提高。

image-20221129204417536

总结:

快速排序不同于归并排序,它虽然也是递归但是它并不是分治算法的思想而是减治算法的思想,因为它在划分子问题上直接处理数组所以并不需要合并这一步骤,所以快速排序算法的思想是分治算法的特例——减治算法(逐渐的缩小待排序的区间)。

递归是一种逆向思维,它并不是直接解决一个较大的原问题,而是把这个原问题转换为规模更小的子问题,然后再逐个的解决这些子问题,子问题解决之后再利用子问题的结果组合成规模更大的子问题的结果,知道原问题得到了解决,这就是一种逆向思维。

如果我们把递归的结构画出来,其实就会发现它就是一个树结构,递归的过程就相当于遍历这个树,遍历的方法就是深度优先遍历,其中我们刚刚学的归并排序和快速排序都需要我们保存暂时还没有解决的问题,保存这些问题的结构就是栈数据结构。所以递归是空间换时间的思想的体现。


感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,快速排序是一种基于分治思想排序算法,它的基本思路是:将一个大问题分解成若干个小问题,分别解决这些小问题,最终将这些小问题的解整合起来得到原问题的解。 具体来说,快速排序的实现过程如下: 1. 选取一个基准元素(pivot),一般选择数组的第一个元素。 2. 将数组中小于等于基准元素的数放到基准元素的左边,大于基准元素的数放到右边。 3. 对基准元素左边的子数组和右边的子数组分别递归进行步骤1和步骤2,直到子数组的长度为1或0。 下面是用 Python 语言实现快速排序的代码: ```python def quick_sort(arr): if len(arr) <= 1: return arr else: pivot = arr[0] left = [x for x in arr[1:] if x <= pivot] right = [x for x in arr[1:] if x > pivot] return quick_sort(left) + [pivot] + quick_sort(right) ``` 在这个代码中,我们首先检查数组的长度是否小于等于1,如果是,直接返回该数组。否则,我们选择数组的第一个元素作为基准元素,然后根据基准元素将数组分成两个子数组:左边的子数组包含小于等于基准元素的元素,右边的子数组包含大于基准元素的元素。最后,我们对左边的子数组和右边的子数组分别递归调用快速排序函数,然后将它们和基准元素拼接起来得到最终的排序结果。 总之,快速排序算法是一个高效的排序算法,它的时间复杂度为O(nlogn),其中n是待排序数组的长度。通过分治思想,我们可以将一个大规模的排序问题转化为若干个小规模的排序问题,从而提高排序算法的效率。 ### 回答2: 快速排序算法是一种基于分治思想排序算法。它将一个数组划分为两个子数组,其中一个子数组的所有元素都小于另一个子数组的所有元素,并对子数组进行递归排序。下面是使用分治法实现快速排序算法的步骤: 1.选择一个基准元素。一般选择数组的第一个元素。 2.将数组分成两个子数组。一个子数组包含所有小于基准元素的元素,另一个子数组包含所有大于基准元素的元素。基准元素将位于两个子数组之间。 3.递归地对两个子数组进行快速排序。 4.合并两个子数组和基准元素,得到排序后的数组。 下面是使用递归思想实现快速排序的例子: ```python def quickSort(arr): if len(arr) <= 1: return arr pivot = arr[0] # 选择第一个元素作为基准 less = [x for x in arr[1:] if x <= pivot] # 小于等于基准的子数组 greater = [x for x in arr[1:] if x > pivot] # 大于基准的子数组 return quickSort(less) + [pivot] + quickSort(greater) # 递归排序并合并子数组和基准元素 ``` 这样,我们就通过分治法实现了快速排序算法。快速排序的时间复杂度为O(nlogn),其中n是数组的长度。递归调用的空间复杂度为O(logn)。快速排序是一种常用的高效排序算法,因为它的平均时间复杂度较低。 ### 回答3: 快速排序是一种常用的排序算法,使用分治法的思想进行实现。其主要分为三个步骤:选择一个基准元素,将数组分割成两部分,一部分所有元素小于基准元素,另一部分所有元素大于基准元素,然后递归对两个子数组进行排序。 具体实现步骤如下: 1. 选择一个基准元素,一般我们选择数组的第一个元素。 2. 设定两个指针left和right分别指向数组的第一个和最后一个元素。 3. 当left指针小于right指针时,进行以下操作: a. 从right指针开始向左扫描,直到找到一个小于基准元素的元素,将其与基准元素交换。 b. 从left指针开始向右扫描,直到找到一个大于基准元素的元素,将其与基准元素交换。 c. 重复步骤a和b,直到left指针大于right指针。 4. 此时基准元素左边的子数组元素都小于基准元素,右边的子数组元素都大于基准元素。将左边的子数组和右边的子数组分别递归调用快速排序算法,直到每个子数组只有一个元素为止。 5. 最终得到一个有序的数组。 由于快速排序使用了递归思想,所以在实现时需要注意递归的退出条件。一般情况下,递归退出的条件是子数组中只有一个元素或者为空数组。 快速排序算法的时间复杂度为O(nlogn),其中n为数组的大小。这是因为每次划分时,都能将数组分成规模接近一半的两个子数组,所以进行对数级别的次数划分,整个算法的时间复杂度就是O(nlogn)。此外,快速排序是原地排序算法,不需要额外的存储空间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值