CSDN话题挑战赛第2期
参赛话题:学习笔记
上一文给大家讲解了排序算法中的选择排序与堆排序,今天,我们来进入交换排序,学习新的两种排序算法——冒泡排序与快速排序
对于冒泡排序,大家应该是经常有听到过,也就是选定一个数与其后面的数作比较,将大的数或是小的数冒上来
//先写一个单趟的内部排序
for (int j = 1; j < n; ++j)
{
if (arr[j - 1] > arr[j])
{
swap(arr[j - 1], arr[j]);
}
}
说完内层循环,接下去来说说套在外层的循环
for (int i = 0; i < n - 1; i++)
{
//先写一个单趟的内部排序
for (int j = 1; j < n - i; ++j)
{
if (arr[j - 1] > arr[j])
{
swap(arr[j - 1], arr[j]);
}
}
PrintArray(arr, n);
}
小结:
了解了边界如何去计算,但是从上图我们可以看出,其实这个数字在进行了四次排序后就已经结束了,完成了升序排序,但是因为外部的循环还没有到末尾,因此还会进行一个继续的比较,我们来将其优化一下吧
代码
for (int i = 0; i < n - 1; i++)
{
int exchanged = 0;
//先写一个单趟的内部排序
for (int j = 0; j < n - i - 1; ++j)
{
if (arr[j] > arr[j + 1])
{
swap(arr[j], arr[j + 1]);
exchanged = 1;
}
//PrintArray(arr, n);
}
/*优化,若是已经有序,则跳出循环*/
if (!exchanged) break;
PrintArray(arr, n);
}
就这样看代码可能也没有对内部的实现有一个很好的体会,接下来我们通过步步图解以及DeBug调试来感受一下机器思维
讲完了冒泡排序,接着我们来讲一种大家很喜欢用的排序——快速排序✈
说到快速排序,首先要给大家介绍的就是最经典的挖坑法
int begin = 0, end = n - 1;
int pivot = begin;
int key = a[begin]; //对照值
那具体应该怎么去进行一个比较呢?我们来看一下代码
while (begin < end)
{
//右边找小,放到左边
while (a[end] >= key)
{
--end;
}
a[pivot] = a[end];
pivot = end;
//左边找大,放到右边
while (a[begin] <= key)
{
++begin;
}
a[pivot] = a[begin];
pivot = begin;
}
pivot = begin;
a[pivot] = key;
我们来看一下运行结果吧
那这个时候应该怎么办呢?那就又要出动我们的DeBug了,开始步步带大家调试
专门给大家录了个视频讲解,可以更加清晰地感受这个交换的逻辑,温馨提示:微信端看不到,可能有一些杂音
挖坑法讲解
上面我们只完成了第一步,也就是保持key值的左区间比它小,保持key值的右区间比它大,那接下去我们要怎么进行排序呢?
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left, end = right;
int pivot = begin;
int key = a[begin]; //对照值
while (begin < end)
{
//右边找小,放到左边
while (begin < end && a[end] >= key)
{
--end;
}
a[pivot] = a[end];
pivot = end;
//左边找大,放到右边
while (begin < end && a[begin] <= key)
{
++begin;
}
a[pivot] = a[begin];
pivot = begin;
}
pivot = begin;
a[pivot] = key;
//[left,right]
//[left,pivot - 1] pivot [pivot + 1,right]
//左子区间和右子区间有序,我们就有序了,如何有序?分治递归
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot + 1, right);
}
以下是原理图
对于快速排序,它的时间复杂度是多少呢?我们一起来分析一下
while (begin < end)
{
//右边找小,放到左边
while (begin < end && a[end] >= key)
{
--end;
}
a[pivot] = a[end];
pivot = end;
//左边找大,放到右边
while (begin < end && a[begin] <= key)
{
++begin;
}
a[pivot] = a[begin];
pivot = begin;
}
但是我们从下面这张【排序复杂度分析图】看出,快速排序我们上次讲的堆排很类似,但是快排的最坏情况却是O(n2),这是为什么呢?我们来探究一下:
上面讲到了快排在数据已经有序的情况下时间复杂度会提升到O(n2),但光是这样说大家可能不太信服,我们来测试一下
void swap(int& x, int& y)
{
int t = x;
x = y;
y = t;
}
void PrintArray(int* arr, int n)
{
for (int i = 0; i < n; ++i)
{
cout << arr[i] << " ";
}
cout << endl;
}
/*直接插入排序*/
void InsertSort2(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end]) {
a[end + 1] = a[end];
--end;
}
else {
break;
}
}
a[end + 1] = tmp;
//PrintArray(a, n);
}
}
/*希尔排序*/
void Shell_Sort2(int* arr, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2; //log2N
/*
* gap > 1时都是预排序 —— 接近有序
* gaop = 1 时为直接插入排序 —— 有序
*/
//gap很大时,下面预排序时间复杂度O(N)
//gap很小时,数组已经接近有序,这时差不多也是(N)
//把间隔为gap的多组数据同时排
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end]) {
arr[end + gap] = arr[end];
end -= gap;
}
else {
break;
}
}
arr[end + gap] = tmp;
}
//PrintArray(arr, n);
}
}
/*选择排序*/
void Select_Sort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = end;
for (int i = begin; i < end; ++i)
{
/*更新最大最小值*/
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
//将最小值放在最前面,将最大值放在最后面
swap(a[begin], a[mini]);
swap(a[end], a[maxi]);
begin++;
end--;
}
}
/*堆排序*/
void Adjust_Down(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
//选出左右孩子中小的那一个
if (child + 1 < n && a[child + 1] > a[child])
{ //考虑到右孩子越界的情况
child += 1;
//若右孩子来的小,则更新孩子结点为小的那个
}
//交换父亲节点和小的那个孩子结点
if (a[child] > a[parent]) {
swap(a[child], a[parent]);
//重置父亲节点和孩子结点
parent = child;
child = parent * 2 + 1;
}
else { //若已是小根堆,则不交换
break;
}
}
}
void Heap_Sort(int* a, int n)
{
//建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
Adjust_Down(a, n, i);
//排升序,建大堆
int end = n - 1; //获取最后一个叶子结点
while (end > 0)
{
swap(a[0], a[end]); //将第一个数与最后一个数交换
Adjust_Down(a, end, 0); //向下调整,选出次大的数,再和倒数第二个数交换
end--; //最后一个数前移,上一个交换完后的数不看做堆中的数
}
}
/*冒泡排序*/
void Bubble_Sort3(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int exchanged = 0;
//先写一个单趟的内部排序
for (int j = 0; j < n - i - 1; ++j)
{
if (arr[j] > arr[j + 1])
{
swap(arr[j], arr[j + 1]);
exchanged = 1;
}
//PrintArray(arr, n);
}
/*优化,若是已经有序,则跳出循环*/
if (!exchanged) break;
//PrintArray(arr, n);
}
}
/*快速排序*/
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left, end = right;
int pivot = begin;
int key = a[begin]; //对照值
while (begin < end)
{
//右边找小,放到左边
while (begin < end && a[end] >= key)
{
--end;
}
a[pivot] = a[end];
pivot = end;
//左边找大,放到右边
while (begin < end && a[begin] <= key)
{
++begin;
}
a[pivot] = a[begin];
pivot = begin;
}
pivot = begin;
a[pivot] = key;
//[left,right]
//[left,pivot - 1] pivot [pivot + 1,right]
//左子区间和右子区间有序,我们就有序了,如何有序?分治递归
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot + 1, right);
}
void TestOP()
{
srand(time(0));
const int N = 50000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
}
int begin1 = clock();
InsertSort2(a1, N); //直接插入排序
int end1 = clock();
int begin2 = clock();
Shell_Sort2(a2, N); //希尔排序
int end2 = clock();
int begin3 = clock();
Select_Sort(a3, N); //选择排序
int end3 = clock();
int begin4 = clock();
Heap_Sort(a4, N); //堆排序
int end4 = clock();
int begin5 = clock();
Bubble_Sort3(a5, N); //冒泡排序
int end5 = clock();
int begin6 = clock();
QuickSort(a6, 0, N - 1); //快速排序
int end6 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("Bubble_Sort3:%d\n", end5 - begin5);
printf("QuickSort:%d\n", end6 - begin6);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
}
int begin5 = clock();
Bubble_Sort3(a5, N); //冒泡排序
int end5 = clock();
int begin6 = clock();
QuickSort(a5, 0, N - 1); //快速排序
int end6 = clock();
注意事项:测试性能记得在Release版本下,会有一些优化,DeBug版本数据太大可能会出现栈溢出的,亲测/(ㄒoㄒ)/~~
但是尽管是这样,快速排序使用的人还是那么多,这是为什么呢?我们把数据量改到100000
讲完一种快速排序的方法,并且用它与其他排序算法完成了性能测试,接下来我们再来学习一种快速排序的方法,叫做【左右指针法】,它是挖坑法延伸出来的,与挖坑法非常得类似
//单趟排序
int PartSort1(int* a, int left, int right)
{
int index = GetMid(a, left, right); //获取中间值
swap(a[left], a[index]); //将中间值换到第一位上
int begin = left, end = right;
int pivot = begin;
int key = a[begin]; //对照值
while (begin < end)
{
//右边找小,放到左边
while (begin < end && a[end] >= key)
{
--end;
}
a[pivot] = a[end];
pivot = end;
//左边找大,放到右边
while (begin < end && a[begin] <= key)
{
++begin;
}
a[pivot] = a[begin];
pivot = begin;
}
pivot = begin;
a[pivot] = key;
return pivot; //返回坑的位置
}
/*【挖坑法】左右小区间法——小型优化*/
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyindex = PartSort1(a, left, right); //获取坑位
//[left,right]
//[left,keyindex - 1] keyindex [keyindex + 1,right]
//左子区间和右子区间有序,我们就有序了,如何有序?分治递归
//左子区间
if (keyindex - 1 - left > 10)
{ //若数据量 > 10,则继续递归
QuickSort(a, left, keyindex - 1);
}
else
{ //若数据量 <= 10,则开始优化
InsertSort2(a + left, keyindex - 1 - left + 1);
//数组首元素地址 数组个数
}
//右子区间
if (right - (keyindex + 1) > 10)
{ //若数据量 > 10,则继续递归
QuickSort(a, keyindex + 1, right);
}
else
{ //若数据量 <= 10,则开始优化
InsertSort2(a + keyindex + 1, right - (keyindex + 1) + 1);
//数组首元素地址 数组个数
}
}
接下去我们来说一下左右指针法
//单趟排序
int PartSort2(int* a, int left, int right)
{
int index = GetMid(a, left, right); //获取中间值
swap(a[left], a[index]); //将中间值换到第一位上
int begin = left, end = right;
int key = begin; //对照值
while (begin < end)
{
//右边找小,放到左边r
while (begin < end && a[end] >= a[key])
{
--end;
}
//左边找大,放到右边
while (begin < end && a[begin] <= a[key])
{
++begin;
}
swap(a[begin], a[end]);
}
swap(a[begin], a[key]);
return begin;
}
下面是动画展示,更加形象一点
讲完了左右指针法,再来讲一种叫做前后指针法,这种方法其实思想也和上两种差不多,也是两个指针在移动,不过这种是同时向后移动,让我们一起来看一下
int PartSort3(int* a, int left, int right)
{
int key = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[key])
{
prev++;
swap(a[prev], a[cur]);
}
cur++;
}
swap(a[prev], a[key]);
return prev;
}
最后我们通过动画再来过一遍
看完了上面三种快速排序的,我们可以看出,都是使用递归的方法去实现的,也就是通过一层的遍历找出一个中间值,然后根据这个中间值进行一个左右划分,分别去进行分治递归。可以看出三种方法虽然类似,但都有自己的独特之处
但是大家肯定有一个疑问,既然都已经学了三种方法了,那为什么还要再去学习非递归的写法呢?我们来探究一下
那非递归改递归这一块要怎么实现呢?我们来看一下
先给出整体代码
void QuickSort_NoRecursive(int* a, int n)
{
/*堆栈的逆序思维*/
ST st;
InitStack(&st);
Push(&st, n - 1); //先入右区间
Push(&st, 0); //再入左区间
while (!StackEmpty(&st)) //直到栈为空
{
//此时栈顶先为左区间,再为右区间
//先获取左区间
int left = Top(&st);
Pop(&st);
//再获取右区间
int right = Top(&st);
Pop(&st);
int keyindex = PartSort1(a, left, right); //传入区间值获取中间值
//[left,keyindex - 1] keyindex [keyindx + 1,right]
//若区间还未有序,则继续入栈出栈,使区间有序
//1.先是右区间
if (keyindex + 1 < right)
{
Push(&st, right);
Push(&st, keyindex + 1);
}
//2.再是左区间
if (left < keyindex - 1)
{
Push(&st, keyindex - 1);
Push(&st, left);
}
}
}
然后我们来分步讲解一下
ST st;
InitStack(&st);
Push(&st, n - 1); //先入右区间
Push(&st, 0); //再入左区间
//此时栈顶先为左区间,再为右区间
//先获取左区间
int left = Top(&st);
Pop(&st);
//再获取右区间
int right = Top(&st);
Pop(&st);
int keyindex = PartSort1(a, left, right); //传入区间值获取中间值
//[left,keyindex - 1] keyindex [keyindx + 1,right]
//若区间还未有序,则继续入栈出栈,使区间有序
//1.先是右区间
if (keyindex + 1 < right)
{
Push(&st, right);
Push(&st, keyindex + 1);
}
//2.再是左区间
if (left < keyindex - 1)
{
Push(&st, keyindex - 1);
Push(&st, left);
}
大家下去可以自己DeBug一下,这里就不带大家做了,自行体会一下它究竟是如何运行的,你就会懂得其中的原理
分析了快速排序的时间复杂度,知道了它有一个小缺陷,就是当数组有序时,快速排序的时间复杂度会从O(NlogN)上升到O(n2),接近与冒泡排序,但是有没有方法可以对齐进行优化呢?那一定是有的,我们一起来看一下吧
int mid = (left + right) >> 1;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
{ //left mid right
return mid; //此时mid便为中间值,返回即可
}
else if (a[mid] > a[right])
{
if (a[left] < a[right])
{
return right;
} //left right mid
else
{
return left;
} //right left mid
}
}
else //a[left] >= a[mid]
{
if (a[mid] > a[right])
{ //right mid left
return mid;
}
else if (a[left] < a[right])
{ //mid left right
return left;
}
else
{ //mid right left
return right;
}
}
上面了解了如何去优化快速排序,接下来我们来测试一下这个代码逻辑是不是真的可以实现性能优化
int index = GetMid(a, left, right); //获取中间值
swap(a[left], a[index]); //将中间值换到第一位上
优化前
优化后(这里冒泡注释掉了,换了个地方测试)
运用三数取中法,对快速排序进行了一个优化,接下去我们再来将一种优化方式,叫做左右小区间法
对于三数取中法,是在开头优化;对于左右小区间法,则是在结尾优化
好,这里给大家【简单】画了一张图,其实随着这个递归次数的增加,递归的层层深入,这个数据量也会被倍增,那么这个程序所需要消耗的内存就会越多,那我们有没有办法将最后的这几层递归消除呢?
这就要用到这个【左右小区间法】,什么叫左右小区间法呢?也就是随着这个区间被不断地划分,到了最后的那么几个区间,比如说每个区间只剩十个数的时候,我们就考虑将这个区间内的数再进行一个排序
那这个时候还是用快排吗,当然不是?如果用快排的话那和继续递归下去就没什么两样了
我们要使用其他的、用着此处最合适的排序算法,首先排除冒泡、选择,O(N2)的肯定不要,堆排序还要建堆,虽然性能可观,但只会增加繁琐度。那用什么,用希尔吗?不,这个地方不能用希尔,因为这个地方的数据量大概只有10个左右,并不多,希尔排序的话最好是用在数据量较大的地方,这样才可以凸显出其优势。一个个排除下来,最后只剩下直接插入排序了,对,就是用它,虽然在有些场合下直接插入排序的性能不是很优,但是在此处只有10个数的情况,我们用直接插入排序最为合适
//左子区间
if (pivot - 1 - left > 10)
{ //若数据量 > 10,则继续递归
QuickSort2(a, left, pivot - 1);
}
else
{ //若数据量 <= 10,则开始优化
InsertSort2(a + left, pivot - 1 - left + 1);
//数组首元素地址 数组个数
}
//右子区间
if (right - (pivot + 1) > 10)
{ //若数据量 > 10,则继续递归
QuickSort2(a, pivot + 1, right);
}
else
{ //若数据量 <= 10,则开始优化
InsertSort2(a + pivot + 1, right - (pivot + 1) + 1);
//数组首元素地址 数组个数
}
小区间优化后
小区间优化前
最后感谢您对本文的观看,如有问题请于评论区留言或者私信我