数据结构之八大算法详解(2)——快速排序,归并排序
快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
hoare版本
单趟排序:
- 选择一个key(一般是一个或者最后一个)
- 单趟排序,要求小的在key的左边,大的在key的右边!
右边先开始找小,找到小的就停下来。然后左边开始找大,找到大的就停下俩,然后两个互换!再继续从右边开始,以此循环,最后直到两个相遇!与key进行交换!
左边做如何保证最后的相遇点一定比key要小呢?答案是右边先走保证的!
我们可以分为两种情况
-
R停下来,L撞到R相遇,相遇位置比key要小
挖坑法
int PartSort2(int* a, int left, int right)//单趟排序! { int mid = GetMidIndex(a, left, right); swap(&a[mid], &a[left]); int key = a[left]; int hole = left;//用来保存坑位! while (left < right) { while (left < right && a[right] >= key) { --right; } a[hole] = a[right]; hole = right; while (left < right && a[left] <= key) { ++left; } a[hole] = a[left]; hole = left; } int meeti = left; a[left] = key; return meeti; } void QuickSort(int* a, int begin, int end) { if (begin >= end) { return; } if (end - begin <= 8) { InsertSort(a + begin, begin - end + 1); } else { int keyi = PartSort2(a, begin, end); QuickSort(a, 0, keyi - 1); QuickSort(a, keyi + 1, end); } }
前后指针法
cur ——作用是找小!
prev的两种状态
1.紧跟着 cur 2. 在比key大的位置的前面!(prev和cur之间都是比key还要大的!)
int PartSort3(int* a, int left, int right) { int mid = GetMidIndex(a, left, right); swap(&a[mid], &a[left]); int keyi = left; int prev = left; int cur = left+1; while (cur <= right) { if (a[cur] < a[keyi] && cur != prev) { prev++; swap(&a[cur], &a[prev]); } cur++; } swap(&a[keyi],&a[prev]); return prev; } void QuickSort(int* a, int begin, int end) { if (begin >= end) { return; } if (end - begin <= 8) { InsertSort(a + begin, end - begin + 1); } else { int keyi = PartSort3(a, begin, end); QuickSort(a, 0, keyi - 1); QuickSort(a, keyi + 1, end); } }
快速排序的非递归算法!
递归的本质是栈!我们要使用非递归本质也是模仿栈的原理!
从该图我们可以看出来每次进入下一层的递归本质就是区间范围的改变!所以我们可以使用数据结构中的栈来模仿这改变的顺序!
数据结构中的栈存储的就是范围的改变!
//关于栈请读者自行实现! void QuickSortNonR(int* a, int begin, int end) { ST qk; StackInit(&qk); stackPush(&qk, begin); stackPush(&qk, end); //将开始的左右范围放在栈中! while (!StackEmpty(&qk))//只有栈中没有值了就可以停止了! { int right = StackTop(&qk);//因为栈是后进先出所以要先接收right StackPop(&qk); int left = StackTop(&qk); StackPop(&qk); //取出左右区间范围 if (left >= right)//这个就相当于递归中的return! { continue; } int keyi = PartSort3(a, left, right);//在这个取出来的范围进行单趟排序!得到keyi值 //将区间分为3个部分 //[left keyi-1] keyi [keyi+1 right] //我们要先拿出左区间,再拿出右区间,所以先把右区间放进去,再放左区间 //因为栈是后进先出的! stackPush(&qk, keyi + 1);//先放左边 stackPush(&qk, right);//在放右边 //将右区间放进去 先放右区间的左值 再放右区间的右边值! //这样下一次循环 right接收到的就是右值 left接收到的就是左值 stackPush(&qk, left);//先放左边 stackPush(&qk, keyi - 1);//在放右边 //同理! } StackDestroy(&qk); }
快排总结
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
可以用二叉树的前序遍历来理解快速排序,二叉树的前序遍历就是先遍历根,在遍历左数,然后遍历右数
快速排序也是类似,先找出本次的keyi 然后排左区间,然后排右区间!
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序的时间复杂度!
归并排序的本质就是一颗二叉树!
所以层数为logN层,每一层的数据为N
所以其时间复杂度为==O(nlogn)==
空间复杂度为O(N)——因为要借助第三方的数组
归并排序的非递归
归并排序的非递归麻烦在它是一个后序的逻辑!当我们分到最后的时候,归并完毕,我们该如何去寻找上一层的区间进行归并
这就很麻烦了!
所以我们可以考虑不要用栈和队列而是改成循环!
像是斐波那契数列也是一种后序,也可以改成循环!
思路:两两归一,
void MergeSortNonR(int* a, int size) { int* temp = (int*)malloc(sizeof(int) * size); if (temp == NULL) { perror("malloc fail"); } int gap = 1; while (gap < size) { for (int j = 0; j < size; j += 2 * gap) { int begin1 = j, end1 = j + gap - 1; int begin2 = j + gap, end2 = j + 2 * gap - 1; int i = j; while (begin1 <= end1 && begin2 <= end2)//有一个结束就全部结束 { if (a[begin1] <= a[begin2]) { temp[i++] = a[begin1++]; } else { temp[i++] = a[begin2++]; } } while (begin1 <= end1) { temp[i++] = a[begin1++]; } while (begin2 <= end2) { temp[i++] = a[begin2++]; } } memcpy(a, temp, sizeof(int) * size);//整体拷贝存在弊端 gap *= 2; } }
归并排序总结
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
归并排序的最大缺点在于它的这个空间复杂度!所以归并排序的思考更多的是解决在磁盘中的==外排序问题==
外排序——不在内存中进行排序!