引言:
刚刚去回顾了一下递归实现的几个小代码,感觉递归真的是很神奇的一个东西,现在感觉递归确实也是比较不好理解的东西,所以这边我提供几个好的学习和理解递归的方法,递归无非就是两种,一种是求出递归的递归,另一种是起作用的递归函数的递归,两种是有一定的区别的。我们先讲一下要求出递归的递归,这种就类似于用递归求一个数的阶乘,必须要从头开始算,算出了头才可以算尾,这个就叫要求出递归的递归,学习这种递归首先就是要弄明白这个递归的原理,然后再在纸上把每一个递归的值给算出来,当然是要从最头上的那个开始算,另一种就是不需要算的递归,只要可以发挥出函数的作用就可以了,这种递归是比较的抽象的,因为这种作用型递归函数都是非常长的一串代码,所以当我们要理解这种递归的时候,我们可以画出它的递归展开图,就是一幅图一幅图的画,递归一次你就画一幅图,并且在画图理解的同时可以调试代码,按照图来调试代码,一遍不行再一遍,直到理解(这时想要理解这种递归的唯一方法),所以递归可以说是非常的不好搞。现在北京时间 2022/12/27/0:43 ,今天可以看出是超出了平常写博客的时间,但是我们还是要坚持写博客,并且我刚刚看了一下我最开始写的博客,我觉得当时的博客写的很有味道,现在的博客总感觉没什么味道了,以前写博客每一个字,都是肺腑之言一般,现在的博客除了引言中的内容,好像大部分都有一些不够活灵活现的感觉,以前的每一句话写的真的都很好玩的感觉,就算是那种死板的语言也可以被我们自己理解然后写的非常的有趣搞笑,不想现在的,有一些刻板,就像是以前都在写真心话,现在写的都是客套话的感觉,并且有一种以前写的博客是真真实实的写给自己看的,而现在的博客更不像是不是写给自己看的;总感觉现在的博客和以前区别很大,可能是我真的经历了吧!经历了改变了,以前刚开始写博客,写完之后睡觉好像都是想住别人在浏览你的博客,然后你的解释是如何如何的好,语句是多么多么的清楚明了,自己在句里行间写了多少多少的细节在里面,然后别人发现这些细节的时候是怎样的感觉,之类的想法,当时好像都会有,但是现在的我,好像……,可能是明白了CSDN的推荐和不推荐,关注和不关注之类的问题和当今时代文章好像确实是没有人看,更没有人会一个字一个字的仔细看,并且自己作为一个小白,写的内容也都是最基本的,好像确实是不需要看之类的这一系列额想法在里面,最终导致了我们博客风格的改变吧!行文来到这个位置,我接上一篇博客分享一下我的昨晚睡觉体验(如我意料之中,是我许久以来睡的最舒服的一次),缩小了位置,并不会影响我的睡觉体验,但是却可以让我睡的暖和,…… ,天大寒,码字不易,引言就这样吧!今天我们讲一下什么是归并排序和快速排序的另外两种思路。
1.归并排序(MergeSort)
时间复杂度:O(N*logN)
1.1 归并排序原理:我们可以假设一个数组它的左半区间有序 右半区间也有序 ,然后在这个前提之下,然后我们使用归并算法:依次比较左半区间和右半区间中的元素然后取小的元素放到新创建的临时数组中。
按照归并排序的原理,此时我需要有两个新的个区间,如何获得两个区间呢?就是将int mid = (left + right) / 2; 这样我们就可以利用这个中间值进行一个数组分成两个区间,有了区间我们此时就假设左半区间[ left , mid ] 和右半区间 [ mid+1 ,right ] 是有序的,那么我们就可以进行归并,让我的左半区间和右半区间有序(只有这样,才可以进行我的归并算法实现),分治递归实现。
图示如下:
问题:归并之前,左右区间没有序?怎么办?
此时这个问题就涉及到我们归并排序中的递归问题
所以想要解决这个问题就需要我们进行一个分治递归的思想,从而实现我们的代码
所以我们此时如何使我的左半区间和右半区间有序呢?
分治递归:
_MergeSort(arr, left, mid, tmp);//假设我使用归并算法已经把左半区间排成有序了 _MergeSort(arr, mid + 1, right, tmp);//假设我使用归并算法已经把半右区间排成有序了
这样我们就可以让我们的一个数组的左半区间和右半区间都有序了,此时就可以进行归并排序了
具体动图如下:
1.2 归并排序代码实现
#include<stdlib.h> void _MergeSort(int* arr, int left, int right, int* tmp) { if (left >= right)//递归条件 { return; } int mid = (left + right) >> 1; //分治递归 _MergeSort(arr, left, mid, tmp); _MergeSort(arr, mid + 1, right, tmp); //归并算法的代码实现: int begin1 = left; int end1 = mid; int begin2 = mid + 1; int end2 = right; int index = left; while (begin1 <= end1 && begin2 <= end2) { if (arr[begin1] < arr[begin2]) { tmp[index++] = arr[begin1++]; } else { tmp[index++] = arr[begin2++]; } } while (begin1 <= end1) { tmp[index++] = arr[begin1++]; } while (begin2 <= end2) { tmp[index++] = arr[begin2++]; } //将新数组中的元素拷贝回原数组 int i; for (i = left; i <= right; i++) { arr[i] = tmp[i]; } } void MergeSort(int* arr, int n) { int* tmp = (int*)malloc(sizeof(int) * n); _MergeSort(arr, 0, n - 1, tmp); free(tmp); }
注意点: 为了防止待会递归这个函数的时候每一次都还要重新malloc一块空间,所以我们就要把这个函数的作用外包到外面去,不然每次都要malloc一块空间效率下降,所以我就可以在外部重新写一个这个函数的子函数void _MergeSort(int* arr, int left, int right, int* tmp) 并且此时的这个函数其实还是MergeSort函数,只是多了一个 _ 而已,_ 表示的就是子函数的意思
2.快速排序的优化(顶级优化)
(注释详细)
学到这个位置我们意识到了,快排是有一定的缺陷的,当我转参的数组是一个有序的数组的时候(例:1 2 3 4 5 6 7 8 9 ),想要使用快排进行排序(因为快排的原理,每次都是取arr[0]或arr[end-1]),所以就会导致第一次是N,第二次是N-1,……,最后0,然后等差数列相加(就会导致快排的时间复杂度是:O(N^2))
所以快排是有一定的缺陷的,所以为了避免这个缺陷(我们就要对key的值进行优化),就会使用一个叫三数取中的方法来获取key的值:所以我就要对这个快排的代码就行改进
第一个优化:快排的三数取中法优化, 函数如下int GetMidIndex(int* arr, int left, int right)
第二个优化:小区间优化
int GetMidIndex(int* arr, int left, int right) { //三数取中法代码 int mid = (left + right) >> 1;//这句代码=> int mid = (left + right)/2; 只是移位的效率会高一点点 //下面的这些if条件的判断就是为了取到三个数中不是最大也不是最小的那个数而已 if (arr[left] < arr[mid]) { if (arr[mid] < arr[right]) { return mid; } else if(arr[left]>arr[right])//程序来到这个位置就是说明,此时我的mid是我的最大值,然而我的目的是为了找到中间的那个值,所以这边我就比较一下left和right就行,大的那个就是中间值 { return left; } else { return right;//程序来到这里就说明right大,那么right就是中间值,是中间值就return } } else //此时这个else的意思就是:arr[left] > arr[mid] { if (arr[left] < arr[right])//这边三数取中的写法很多,可以有不一样的写法,不怕 { return left; } else if (arr[mid] < arr[right]) { return right; } else { return mid; } } } void QuickSort(int* arr, int left, int right) { if (left >= right) { return; } int index = GetMidIndex(arr, left, right); Swap(&arr[left], &arr[index]);//下面的代码基本不变,就是这两步用来获取我的中间值就行了 int begin = left; int end = right; int pivot = begin; int key = arr[begin]; while (begin < end) { while (begin < end && arr[end] >= key) { end--; } arr[pivot] = arr[end]; pivot = end; while (begin < end && arr[begin] <= key) { begin++; } arr[pivot] = arr[begin]; pivot = begin; } pivot = begin; arr[pivot] = key; //此时代码来到这个位置就有一些不好的地方(比如刚刚我们使用了三数取中的函数,如果此时需要排序的数据非常大(1000000),有这么多个数据,此时如果要一直调用三数取中的函数就会导致效率问题) //并且当我们需要排序的数据剩下的不是非常多的时候(此时正是调用次数最多的时候因为快排是一个logN的排序 越到后面,执行次数越多(例:2^19 2^18)),所以此时我们可以来一个小区间优化 //就是当需要排序的数据没有那么多时,我们就不使用快速排序,我们使用直接插入排序就行 //原递归代码: //QuickSort(arr, left, pivot - 1); //QuickSort(arr, pivot + 1, right); //所以优化代码如下: if (pivot - 1 - left > 10) { QuickSort(arr, left, pivot - 1); } else { InsertSort(arr + left, pivot - 1 - left + 1); } if (right - (pivot + 1) > 10) { QuickSort(arr, pivot + 1, right); } else { InsertSort(arr + pivot + 1, right - (pivot + 1) + 1); } }
3.快速排序的三种思路的代码实现及分析
3.1 快排的第一种思想挖坑法
int PartQuickSort1(int* arr, int left, int right)//此时这个表示的是一个单趟排序的挖坑法快速排序方法 { int index = GetMidIndex(arr, left, right); Swap(&arr[left], &arr[index]);//下面的代码基本不变,就是这两步用来获取我的中间值就行了 int begin = left; int end = right; int pivot = begin; int key = arr[begin]; while (begin < end) { while (begin < end && arr[end] >= key) { end--; } arr[pivot] = arr[end]; pivot = end; while (begin < end && arr[begin] <= key) { begin++; } arr[pivot] = arr[begin]; pivot = begin; } pivot = begin; arr[pivot] = key; return pivot; }
3.2 快排的第二种思路的实现(前后指针法)
int PartQuickSort3(int* arr, int left, int right) { int index = GetMidIndex(arr, left, right); Swap(&arr[left], &arr[index]); int keyi = left; int prev = left; int cur = left + 1; while (cur <= right) { if (arr[cur] < arr[keyi]) { prev++; Swap(&arr[prev], &arr[cur]); } cur++; } Swap(&arr[keyi], &arr[prev]); return prev; }
3.3 快排的第三种方法的实现(挖坑法的变形(左右指针法))
理解:注释很详细
int PartQuickSort2(int* arr, int left, int right) { int index = GetMidIndex(arr, left, right); Swap(&arr[left], &arr[index]); int begin = left; int end = right; int keyi = begin; //左右指针法的快排 while (begin < end)//表示两指针找元素,直到相遇为止 { while (begin < end && arr[end] >= arr[keyi])//就是在右边找比key小的数 { end--; //注意点:记得要有等号,因为如果没有等号就可能会死循环,因为如果左边和右边,两边同时找到一个和keyi相等的数(进不去循环,就导致end和begin不变),此时就会导致这两个数相等的数一直换过来换过去,就会导致死循环 } while (begin < end && arr[begin] <= arr[keyi]) { begin++; } //程序来到这个位置就说明我已经找到了比key大的数和比key小的数了,此时只要把这两个数给交换一下,然后我就完成了快排的单趟排序了(剩下的就交给递归去搞定就行了) Swap(&arr[begin], &arr[end]);//交换完,我就完成了快排的单趟排序了(剩下的就交给递归去搞定就行了) } Swap(&arr[begin], &arr[keyi]);//这步就是为了把我的keyi这个关键位置给放到中间的那个位置(同时也是正确的那个位置) return begin;//就是返回中间位置(虽然可能不是中间,但是就是要中间的意思) }
2.1 快排的三种原理实现测试
void QuickSort2(int* arr, int left, int right) { if (left >= right) { return; } //三种思路的测试(不可以同时调用) int keyIndex = PartQuickSort1(arr, left, right); int keyIndex = PartQuickSort2(arr, left, right); int keyIndex = PartQuickSort3(arr, left, right); if (keyIndex - 1 - left > 10) { QuickSort2(arr, left, keyIndex - 1); } else { InsertSort(arr + left, keyIndex - 1 - left + 1); } if (right - (keyIndex + 1) > 10) { QuickSort2(arr, keyIndex + 1, right); } else { InsertSort(arr + keyIndex + 1, right - (keyIndex + 1) + 1); } }
4.归并排序和快排第3原理的测试