目录
1.小区间优化
测试代码
运行结果
2.非递归的解决方法(重要!)
递归产生的问题
一般来说,递归改非递归有两种方法
算法分析
递归产生的二叉树
栈的示意图
先写代码框架
再填写细节部分
1.小区间优化
回顾121.【C语言】数据结构之快速排序(未优化的Hoare排序存在的问题)以及时间复杂度的分析文章的一般情况下快排的时间复杂度
类似二叉树, 下面分析二叉树的特点
注意到二叉树的最后一层节点个数为近似占总节点个数的一半(有关二叉树计算方面的知识参见100.【C语言】数据结构之二叉树的基本知识文章),因此越往二叉树的下方,递归的次数越多, 一般情况下快排递归近似二叉树,如果能适当减少递归的次数,可以提高效率
解决掉最后一层能减少一半递归次数,解决掉最后三层能减少的递归次数!!
可以想到一个方法:越往二叉树的下方,子数组的元素个数越少,不用递归,用其他的排序方法
★要注意到递归到小区间的特点,数组的大部分元素是有序的(即局部有序),选择合适的排序方法要利用好这一特点★
目前讲过的排序方法有:
1.冒泡排序:118.【C语言】数据结构之排序(堆排序和冒泡排序) 点我跳转
2.选择排序:117.【C语言】数据结构之排序(选择排序) 点我跳转
3.直接插入排序:112.【C语言】数据结构之排序(详解插入排序) 点我跳转
4.希尔排序:115.【C语言】数据结构之排序(希尔排序) 点我跳转
5.堆排序:118.【C语言】数据结构之排序(堆排序和冒泡排序) 点我跳转
下面讲如何选择:
1.首先排除堆排序,堆排序要先建堆,耽误时间
2.再排除希尔排序,希尔排序是对直接插入排序的优化,由于数组的元素个数较少,没有必要使用希尔排序
3.再排除选择排序:无论最好还是最坏情况,时间复杂度都为,而且当是小区间排序时数组的大部分元素是有序的,没有商量的余地,时间复杂度还是
4.冒泡排序 VS 直接插入排序
冒泡排序,最坏情况时间复杂度为,最好情况(有序)时间复杂度为
直接插入排序,最坏情况时间复杂度为,最好情况(有序)时间复杂度为
从时间复杂度上比较,看起来两者没有区别,但是要依据数组的实际情况来看这个问题!
从小区间的特点(数组的大部分元素是有序的(即局部有序:小段小段有序))上来看,选直接插入排序较好,原因:插入排序利用了局部有序这一特点,但是冒泡排序只是机械地将数组中最大、次大......的数依次移动,没有利用这一特点,所以直接插入排序较好
当然也可以稍加改造116.【C语言】测试排序性能的模板代码文章的测试性能的代码来比较冒泡排序和直接插入排序
例如区间长度大于10使用Hoare排序,区间长度小于等于10使用冒泡排序和直接插入排序
测试代码
void QuickSort_Hoare(int* arr, int left, int right)
{if (left >= right)return;if ((right - left + 1) <= 10)//添加这行代码return;//单趟快速排序int begin = left;int end = right;//随机选keysrand((unsigned int)time(0));int rand_i = left+rand() % (right - left);Swap(&arr[left], &arr[rand_i]);int ret = GetMiddleNum(left, right, arr);if (left != ret){Swap(&arr[left], &arr[ret]);}int key_i = left;while (left < right){//由于key_i==left,因此right指针先走//右边找小while (left < right && arr[right] >= arr[key_i]){right--;}//左边找大while (left < right && arr[left] <= arr[key_i]){left++;}Swap(&arr[left], &arr[right]);}Swap(&arr[key_i], &arr[left]);key_i = left;//arr[key_i]和arr[left]交换后下标要改变,否则会对下次递归产生不利结果QuickSort_Hoare(arr, begin, key_i - 1);QuickSort_Hoare(arr, key_i + 1,end);
}void TestTime()
{const int N = 500000;int* a1 = (int*)malloc(sizeof(int) * N);if (a1 == NULL){perror("malloc");return;}int* a2 = (int*)malloc(sizeof(int) * N);if (a2 == NULL){perror("malloc");return;}for (int i = 0; i < N; i++){a1[i] = rand();}QuickSort_Hoare(a1, 0, N - 1);for (int i = 0; i < N; i++){a2[i] = a1[i];}printf("区间长度大于10的排序完毕\n");clock_t begin1 = clock();BubbleSort(a1, N);//BubbleSort函数代码省略clock_t end1 = clock();printf("BubbleSort's time=%ldms\n", end1 - begin1);clock_t begin2 = clock();InsertSort(a2, N);//InsertSort函数代码省略clock_t end2 = clock();printf("InsertSort's time=%ldms\n", end2 - begin2);free(a1);free(a2);
}int main()
{TestTime();return 0;
}
运行结果
从运行的时间上看,插入排序占很大的优势!
则Hoare排序代码应该修改为
void QuickSort_Hoare(int* arr, int left, int right)
{if (left >= right)return;//小区间直接插入排序if ((right - left + 1) > 10){//单趟快速排序int begin = left;int end = right;//随机选keysrand((unsigned int)time(0));int rand_i = left + rand() % (right - left);Swap(&arr[left], &arr[rand_i]);int ret = GetMiddleNum(left, right, arr);if (left != ret){Swap(&arr[left], &arr[ret]);}int key_i = left;while (left < right){//由于key_i==left,因此right指针先走//右边找小while (left < right && arr[right] >= arr[key_i]){right--;}//左边找大while (left < right && arr[left] <= arr[key_i]){left++;}Swap(&arr[left], &arr[right]);}Swap(&arr[key_i], &arr[left]);key_i = left;//arr[key_i]和arr[left]交换后下标要改变,否则会对下次递归产生不利结果QuickSort_Hoare(arr, begin, key_i - 1);QuickSort_Hoare(arr, key_i + 1, end);}else{InsertSort(arr+left, right - left + 1);}
}
注意InsertSort(arr+left, right - left + 1);的写法!!
长度小于等于10的区间不一定都在数组的两端,可能在中间,因此需要提供该区间第一个元素的下标arr+left,一共(right-left+1)各元素,由于是闭区间注意一定要+1!
2.非递归的解决方法(重要!)
递归产生的问题
1.效率(这个影响较小)
2.深度太深,栈会溢出!(严重的问题)
一般来说,递归改非递归有两种方法
1.改循环,可以参见L25.【LeetCode笔记】 三步问题的四种解法(含矩阵精彩解法!)文章和35.【C语言】详解函数递归
2.使用栈辅助改成循环(★)
快速排序算法复杂,改非递归需要用到栈
回顾有关栈的一系列操作
参见99.【C语言】数据结构之栈(含栈的源码)文章
算法分析
递归产生的二叉树
先看递归产生的二叉树的一个图例,用某个算法取出key_i的值
可以看到:快速排序中递归的本质是:区间在变化! 显然可以用栈来存区间
栈的示意图
将上方二叉树图转化为栈图
可以看到类似二叉树的前序遍历:先对该区间快速排序, 相当于访问根,再左区间快速排序,相当于访问左子树,最后访问右区间
(回顾前序遍历知识点参见106.【C语言】数据结构之二叉树的三种递归遍历方式文章),因此将99.【C语言】数据结构之栈文章的栈的源码嵌入快速排序中即可
先写代码框架
栈的初始化和销毁
void QuickSort_Hoare_Use_Stack(int* arr, int left, int right)//非递归,使用栈辅助改循环
{ST st;STInit(&st);//do_somethingSTDestory(&st);
}
再填写细节部分
注意:区间的边界值的入栈顺序和出栈顺序时是有讲究的!
如果先对区间的左边界入栈,再对区间的右边界入栈,那么出栈时,第一次出栈为右边界.第二次出栈为左边界(顺序是反着的!)
同理如果先对区间的右边界入栈,再对区间左边界入栈,那么出栈时,第一次出栈为左边界.第二次出栈为右边界
循环条件:只要栈不为空就继续快速排序
注意:
1.栈里取一段区间,单趟排序
2.单趟分割子区间(左区间和右区间)入栈
3.子区间只有一个值或不存在就不入栈
4.取区间等同于出栈
void QuickSort_Hoare_Use_Stack(int* arr, int left, int right)//非递归,使用栈辅助改循环
{ST st;STInit(&st);STPush(&st, right);STPush(&st, left);while (!STEmpty(&st))//栈不为空继续执行快速排序{//先取栈顶元素,再出栈(这里的左边界和右边界不能改变,之后要入栈)int begin = STTop(&st); STPop(&st);int end = STTop(&st); STPop(&st);int left = begin;int right = end;//随机选keysrand((unsigned int)time(0));int rand_i = left + rand() % (right - left);Swap(&arr[left], &arr[rand_i]);int key_i = left;//Hoare排序while (left < right){//由于key_i==left,因此right指针先走//右边找小while (left < right && arr[right] >= arr[key_i]){right--;}//左边找大while (left < right && arr[left] <= arr[key_i]){left++;}Swap(&arr[left], &arr[right]);}Swap(&arr[key_i], &arr[left]);key_i = left;//左区间 [begin,key_i-1],key_i,右区间[key_i+1,end]//要想出栈时先对左区间排序,后对右区间排序,那么右区间先入栈,左区间后入栈if (key_i + 1 < end){STPush(&st, end);STPush(&st, key_i + 1);}if (begin < key_i - 1){STPush(&st, key_i - 1);STPush(&st, begin);}}STDestory(&st);
}
注意出栈的写法:先保存栈顶元素后将其pop,这一点和汇编指令的pop ax的做法是一样的
pop ax执行过程:
1.将SS:SP指向的内存单元处的数据送入寄存器AX中
2.SP+=2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶