快速排序(代码+详细分析)
目录
基本思想
图解
代码
分割数组的代码
1.hoare版本
2.挖坑法
3.前后指针法
快排代码
1.递归
2.循环
复杂度与稳定性
时间复杂度
空间复杂度
稳定性
完整代码
注意
基本思想
任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
图解
代码
分割数组的代码
1.hoare版本
int PartSort1(int* arr,int left,int right){//hoare版本int end=right-1;//拿到当前数组最后一个元素的下标while(left<end){while(left<end&&arr[left]<=arr[right-1]){//从左往右找到一个大于基准值的值left++;}while(left<end&&arr[end]>=arr[right-1]){//从右往左找到一个小于基准值的值end--;}Swap(&arr[left],&arr[end]);//交换找到的两个元素}Swap(&arr[left],&arr[right-1]);//别忘了最后把基准值放在对应的位置return left;
}
2.挖坑法
原理:给定两个指针begin和end分别指向数组的第一个元素和最后一个元素,此时记录下数组最后一个元素的值充当基准值,然后从数组第一个元素开始往后找到一个比基准值大的元素,用这个元素填充arr[end],end--后,在从后往前找到一个比基准值小的元素,用这个元素来填充arr[begin],begin++后。重复上述过程,直到begin和end相遇,此时将一开始记录下来的值填充到begin和end相遇的位置即可。代码:
int PartSort2(int* arr,int left,int right){int end=right-1;//数组最后一个元素的下标int data=arr[end];//记录的基准值while(left<end){while(left<end&&arr[left]<=data){//从前往后找比基准值大的值left++;}if(left<end){//填充坑位arr[end]=arr[left];end--;}while(left<end&&arr[end]>=data){//从后往前找比基准值小的值end--;}if(left<end){//填充坑位arr[left]=arr[end];left++;}}arr[left]=data;//最后一次坑位填充return left;
}
3.前后指针法
原理:前后指针法的原理不易理解,先直接看代码:
int PartSort3(int* arr,int left,int right){int cur=left;int prev=cur-1;while(cur<right){if(arr[cur]<=arr[right-1]&&++prev!=cur){Swap(&arr[prev],&arr[cur]);}cur++;}return prev;
}
根据上述代码,依然选定数组最后的元素为基准值,会发现在函数执行过程中arr[left]和arr[cur]中间夹的元素都是比基准值大的元素,代码非常巧妙,原理可以意会。
快排代码
1.递归
分割数组的代码进行一次调用后,会按照比基准值小或者大将元素分别排列在基准值的左右,并返回基准值的下标。那么我们可以利用递归,来完成整个快速排序,代码如下:
void QuickSort(int *arr,int left,int right){if(right-left<=1){//元素个数小于一个,不用分割return;}int div=PartSort3(arr,left,right);//分割当前数组QuickSort(arr,left,div);//处理当前数组的左半部分,不包括下标为div的元素QuickSort(arr,div+1,right);//处理当前数组的右半部分,不包括下标为div的元素
}
上述代码相当于一次将一个元素放到对应的位置,经过不断递归,将所有的元素放到想对应的位置。此时发现,快排的代码非常像二叉树的前序遍历。
2.循环
递归的代码虽然简单,但是如果元素的个数很多,那么很有可能因为递归深度过深,而将栈上的空间用光,导致栈溢出。此时可以在递归的代码上改进,递归到一定深度后,采用其他的排序方式,这算是一种优化。但是可不可以从一开始就不递归呢?答案是肯定的,此时递归转循环,我们需要借助栈(一种数据结构,和上述的存储空间的栈不同)这种结构。在C语言中,栈没有在库函数中提供,需要我们自己写,假设栈已经存在,那么非递归的代码如下:
void QuickSortNonR(int* arr,int left,int right){Stack s;//定义栈StackInit(&s);//初始化栈StackPush(&s,left);//数组最左侧下标入栈StackPush(&s,right);//right代表元素个数,入栈while (!StackEmpty(&s)){//栈不空,一直执行下述操作right = StackTop(&s);//获取栈顶元素StackPop(&s);//栈顶元素出栈left = StackTop(&s);StackPop(&s);if (right-left>1){//元素个数大于1,在分割int div = PartSort3(arr, left, right);StackPush(&s, left);StackPush(&s, div);StackPush(&s, div + 1);StackPush(&s, right);}}
}
在非递归的代码中,利用了栈这种结构将数组的元素下标保存起来,实现了与递归相同的功能,但是大大节省了空间。入栈时先左后右,但是出栈是相反的过程,注意栈的特性。具体实现过程阅读代码后就可以深刻理解了。
复杂度与稳定性
时间复杂度
排序的过程如下图,每次排好一个元素:
整个递归的深度为logN,每一层进行分割时的复杂度为O(N),所以时间复杂度就是O(NlogN)。非递归方式和递归时大致一样,时间复杂度也为:O(NlogN)
空间复杂度
递归时深度为logN,所以空间复杂度为:O(logN)。
稳定性
元素有跨区间交换,不稳定。
完整代码
#include <stdio.h>
#include <windows.h>
#include <assert.h>
#include <malloc.h>
#define CAP_TY 2
typedef int DataType;
typedef struct Stack{//栈DataType *array;int top;int capicity;
}Stack;
void StackInit(Stack *ps){//初始化栈assert(ps);ps->array = (DataType *)malloc(sizeof(DataType)*CAP_TY);if (ps->array == NULL){perror("malloc");exit(0);}ps->top = 0;ps->capicity = CAP_TY;
}
int StackEmpty(Stack *ps){//判空return ps->top == 0;
}
void StackDestroy(Stack *ps){//销毁栈assert(ps);if (ps->array != NULL){free(ps->array);ps->array = NULL;ps->top = 0;ps->capicity = 0;}
}
void Checkcapicity(Stack *ps){//判断栈满并且扩容if (ps->top == ps->capicity){ps->array = (DataType *)realloc(ps->array, sizeof(DataType)*(ps->capicity + 2));if (ps->array == NULL){perror("relloc");exit(0);}ps->capicity = ps->capicity + 2;}
}
void StackPush(Stack *ps, DataType data){//入栈Checkcapicity(ps);ps->array[ps->top] = data;ps->top++;
}
void StackPop(Stack *ps){//出栈assert(ps);if (StackEmpty(ps)){return;}ps->top--;
}
DataType StackTop(Stack* ps){//获取栈顶元素assert(!StackEmpty(ps));return ps->array[ps->top - 1];
}void Swap(int* x,int*y){int temp=*x;*x=*y;*y=temp;
}
int PartSort1(int* arr,int left,int right){//hoare版本int end=right-1;//拿到当前数组最后一个元素的下标while(left<end){while(left<end&&arr[left]<=arr[right-1]){//从左往右找到一个大于基准值的值left++;}while(left<end&&arr[end]>=arr[right-1]){//从右往左找到一个小于基准值的值end--;}Swap(&arr[left],&arr[end]);//交换找到的两个元素}Swap(&arr[left],&arr[right-1]);//别忘了最后把基准值放在对应的位置return left;
}
int PartSort2(int* arr,int left,int right){int end=right-1;//数组最后一个元素的下标int data=arr[end];//记录的基准值while(left<end){while(left<end&&arr[left]<=data){//从前往后找比基准值大的值left++;}if(left<end){//填充坑位arr[end]=arr[left];end--;}while(left<end&&arr[end]>=data){//从后往前找比基准值小的值end--;}if(left<end){//填充坑位arr[left]=arr[end];left++;}}arr[left]=data;//最后一次坑位填充return left;
}
int PartSort3(int* arr,int left,int right){int cur=left;int prev=cur-1;while(cur<right){if(arr[cur]<=arr[right-1]&&++prev!=cur){Swap(&arr[prev],&arr[cur]);}cur++;}return prev;
}
void QuickSort(int *arr,int left,int right){//快排递归if(right-left<=1){//元素个数小于一个,不用分割return;}int div=PartSort3(arr,left,right);//分割当前数组QuickSort(arr,left,div);//处理当前数组的左半部分,不包括下标为div的元素QuickSort(arr,div+1,right);//处理当前数组的右半部分,不包括下标为div的元素
}
void QuickSortNonR(int* arr,int left,int right){Stack s;//定义栈StackInit(&s);//初始化栈StackPush(&s,left);StackPush(&s,right);while (!StackEmpty(&s)){//栈不空,一直执行下述操作right = StackTop(&s);StackPop(&s);left = StackTop(&s);StackPop(&s);if (right-left>1){//元素个数大于1,在分割int div = PartSort3(arr, left, right);StackPush(&s, left);StackPush(&s, div);StackPush(&s, div + 1);StackPush(&s, right);}}
}
void Print(int* arr,int size){int i=0;for(;i<size;i++){printf("%d ",arr[i]);}printf("\n");
}
int main(){int arr[]={2,7,8,5,4,1,3,9,0,6};QuickSortNonR(arr,0,sizeof(arr)/sizeof(arr[0]));Print(arr,sizeof(arr)/sizeof(arr[0]));system("pause");return 0;
}
注意
在以上代码中,总是选取数组最右侧的数据作为基准值,假如这组数据是降序的,要排升序,此时依照上述方法进行排序,每次的基准值都是当前数组中的最小值,这时递归的深度为n-1,那么时间复杂度将退化为O(N^2),空间复杂度退化为O(N)。
快速排序(代码+详细分析)相关推荐
- Blueprint代码详细分析-Android10.0编译系统(七)
摘要:Blueprint解析Android.bp到ninja的代码流程时如何走的? 阅读本文大约需要花费18分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Andro ...
- seq2seq翻译任务代码详细分析
文章目录 题目 代码 总结 题目 ''' Description: seq2seq代码详细分析 Autor: 365JHWZGo Date: 2021-12-16 19:59:38 LastEdito ...
- linux加密模块,Linux加解密支持模块代码详细分析之演示验证方案1实验代码及结果...
原标题:Linux加解密支持模块代码详细分析之演示验证方案1实验代码及结果 3.1.5.实验代码 #include #include #include #include #include #inclu ...
- [细读经典]Megatron论文和代码详细分析(1)
[细读经典]Megatron论文和代码详细分析(1) 导航: 迷途小书僮:[细读经典]Megatron论文和代码详细分析(2)102 赞同 · 41 评论文章正在上传-重新上传取消 前言 作为一款支持 ...
- 4.imx6 IPU代码详细分析
4.0 ipu_soc,ipu_channel_t ,ipu_channel_params_t结构体详解 1.ipu_soc结构体: struct ipu_soc { unsigned int id; ...
- uboot代码详细分析.pdf
目录 u-boot-1.1.6 之cpu/arm920t/start.s 分析 ............................................................ ...
- 区块链实现代码详细分析(Python)
代码 import hashlib import json import requests from textwrap import dedent from time import time from ...
- Android RIL代码详细分析
RIL代码分析 代码位于:android/hardware/ril 1 rild.c中的main()函数提供了rild的入口 首先,通过main函数的传参,cmdline,内核选项等方式获取rild. ...
- R语言曲面拟合代码详细分析(1)
#代码参考<R语言数据可视化之美-专业图表绘制指南>例4.2.1,主要代码链接参见 #https://github.com/EasyChart/Beautiful-Visualizatio ...
最新文章
- 【转载】究竟啥才是互联网架构“高可用”
- java:不同数据类型的转换规则
- linux软链接的创建、删除和更新
- 牛津英语3a_空中课堂 | 牛津英语3A学霸笔记 (全) 附3B
- Red5 9的安装配置以及AS3连接red5简单示例 .
- asp.net mvc4 mysql_ASP.NET MVC4 with MySQL: Configuration Error (MySql.Web.v20)
- Windows API一日一练(17)DialogBox和DialogBoxParam函数
- Python+Opencv身份证号码区域提取及识别
- JavaScript--对象类型详解
- 类似组卷网实现快速组卷功能,实现试题,试卷,课件快速录入、搜索、分类查询,支持mathtype和latex2word。
- OkHttp简单封装
- 嵌入式行业技术思维导图
- 用java处理speex编码/解码
- 凸包问题--旋转卡壳
- DOS中del和rd的区别
- SRE重案调查组 第二集 | 挖掘应用处理变慢的“真相”
- wince 默认输入法_wince下中文输入法
- 智力题解题报告 No.3 计算24点
- win10系统U盘安装详细经验附系统软件
- 微信小程序 组件无法使用全局样式