排序:将一组杂乱无章的数据按照一定的规律(升序或者降序)组织起来
排序码:用来作为排序的依据,是数据元素的一个属性域
排序算法的稳定性:符合排序码的元素相对位置不发生变化
各种常见排序算法的分类:
1.冒泡排序:
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
算法思路:
(1)按照升序规则,从前往后冒:
[0, bound) 是等待排序的区间
[bound, size) 是已经有序的区间 (bound,cur 从 0 开始,bound < size -1, cur > size - bound - 1,cur 从前往后,要小于有效区间)
从第一个出发,碰见比自己大的就交换位置,交换后继续向后走,直到走到最后,这个时候最大的元素就在最后,第二次还是从第一个开始冒,碰见比自己大的就交换,直到比较倒数第二个(因为最后一个已经是最大的了,不需要在比较)……直到每个元素都比较,每次冒出一个未排序区间最大的元素,下一次就减少一次比较。第一重循环就是一共有多少个元素,第二重循坏是一个元素要比较多少次。
// 交换函数
void Swap1(int* a, int* b)
{
*a = *a ^ *b;
*b = *b ^ *a;
*a = *a ^ *b;
return;
}
void Swap2(int* a, int* b)
{
*a = *a + *b;
*b = *a - *b;
*a = *a - *b;
return;
}
// (以下所有代码使用的交换函数无特别标注,都是Swap3 )
void Swap3(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
// 冒泡排序
// 从前向后冒,降序
// [bound, size) 代表有序区间
// [0, bound) 代表等待排序区间
// cur = 0 从前向后排序
void BubbleSort2(int arr[], int size)
{
if(arr == NULL || size <= 0){
return;
}
int bound = 0;
for(; bound < size - 1; bound ++){
int cur = 0;
for(; cur < size - bound - 1; cur ++){
if(arr[cur] < arr[cur + 1]){
//Swap2(&arr[cur], &arr[cur + 1]);
Swap3(&arr[cur], &arr[cur + 1]);
}
}
}
return;
}
(2)按照降序规则,从后往前冒:
[0, bound) 是已经有序的区间
[bound, size) 是等待排序的区间 (bound 从0 开始,cur 从 size - 1 开始,bound < size - 1, cur > bound,cur 从后往前,要大于有效区间)
从最后一个出发,碰见比自己大的就交换位置,交换后继续向前走,直到走到第一个元素,此时第一个元素就是最大的,第二次还是从最后一个开始,直到第二个位置(不需要和第一个比较),这样一直到最后一个,每冒一次,下次就少比较一次。
// 从后往前冒泡,升序
// [0, bound) 代表有序区间
// [bound, size) 代表等待排序区间
// cur = size - 1 从后向前排序
void BubbleSort1(int arr[], int size)
{
if(arr == NULL || size <= 0){
return;
}
int bound = 0;
for(; bound < size - 1; bound ++){
int cur = size - 1;
for(; cur > bound; cur --){
if(arr[cur - 1] > arr[cur]){
Swap1(&arr[cur - 1], &arr[cur]);
}
}
}
return;
}
2.选择排序:
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
算法思路:
(从前向后,升序)每次从边界出发,第一个元素作为擂主(不动),和第二个元素进行比较,如果擂主小,那么不交换值,cur 继续向后走,下一个值继续和擂主比,直到最后一个元素和擂主比较完,这个时候第二个元素称为擂主,从第三个元素开始比较,直到比到最后。每一次打擂台都有可能打乱原有符合排序规则元素之间的原有位置,所以不稳定。
// 选择排序 升序
// [0, bound) 是有序区间
// [bound, size) 是待排序区间
void SelectSort(int arr[], int size)
{
if(arr == NULL || size <= 0){
return;
}
int bound = 0;
for(; bound < size; bound ++){
int cur = bound + 1;
for(; cur < size; cur ++){
if(arr[bound] > arr[cur]){
Swap(&arr[bound], &arr[cur]);
}
}
}
return;
}
3.插入排序:
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
算法思路:
(升序) [0, bound) 是已经有序的区间,[bound, size) 是等待排序的区间,bound从 1 开始,cur (cur = bound )来辅助理解完成,前面为一个有序的区间,每次从bound 位置的元素,和有序区间里的元素进行比较,找到一个合适的位置时(边找边搬运,有一个元素比 bound大,就往后搬运一次,直到空出来位置),使用cur来搬运元素,向后搬运,给前面留出一个空位,要提前保存当前bound 的值(搬运会覆盖掉现在的bound),直到搬运到合适的空位置时,cur不再搬运,跳出循环,此时 cur 的位置是空位,用之前保存的 bound值填上空位,直到bound == size 结束大循环,可以理解为所有的元素插入结束。
特点:(1)如果待排序序列有序性比较高时,比较的次数就少,所以效率就比较高;
(2)如果待排序列的元素个数较少时,比较的次数也比较少,此时效率也比较高。
// 插入排序 升序
// [0, bound) 为有效区间
// [bound, size) 为待排序区间
void InsertSort(int arr[], int size)
{
if(arr == NULL || size <= 1){
return;
}
int bound = 1;
for(; bound < size; bound ++){
int bound_value = arr[bound];
int cur = bound;
for(; cur > 0; cur --){
if(bound_value < arr[cur - 1]){
arr[cur] = arr[cur - 1];
}else{
break;
}
}
arr[cur] = bound_value;
}
return;
}
4.堆排序:
时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
算法思路:
创建堆的方式有两种:
(1)上浮式更新创建:上浮就是让叶子节点向上去找合适的位置,数组的大小为 size ,堆有效结点为 0,从数组的第一个元素开始,插入堆中size++,依照规则(大小堆的父子节点大小规则),当前插入元素和其父节点比较,符合不规则进行交换,child 记录叶子节点的下标,parent记录其父节点的下标,交换后更新 child、parent下标,继续比较,符合的话,继续插入下一个元素(插入到堆的最后一个节点,size++),再与其父节点进行比较,直到对的有效节点个数等于数组的元素个数,堆构建完成。
// 上浮式调整
void AdjustUp(int arr[], int arr_size, int index)
{
int child = index;
int parent = (index - 1) / 2;
while(child < arr_size){
if(arr[child] > arr[parent]){
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}else{
break;
}
}
int i = 0;
for(; i < arr_size; i ++){
printf("%d ", arr[i]);
}
printf("\n");
return;
}
// 上浮式调整创建堆
void CreateHeapByUp(int arr[], int arr_size)
{
int index = 0;
for(; index < arr_size; index ++){
AdjustUp(arr, arr_size, index);
}
return;
}
(2)下沉式更新创建:下沉式调整就是让父节点向下去找合适的位置,假设数组中保存的是一个完全二叉树结构,需要进行调整来完成以一个堆,从最后一个父节点(下标为 ((size-1)-1)/2 )开始,parent记录当前父节点的下标,child 记录其孩子节点中较大的一个子树下标,依照规则比较,交换到合适位置后,再开始下沉前一个父节点,直到堆顶元素也下沉完成后,堆就完成了。
// 下沉式调整
void AdjustDown(int arr[], int heap_size, int index)
{
// 第一次调整时,父节点是最后一个父节点
int parent = index;
int child = 2 * parent + 1;
while(child < heap_size){
// 父节点进行下沉时,如果父节点有左右子树,需要比较左右子树的大小,交换较大的子树
if(child + 1 < heap_size && arr[child + 1] > arr[child]){
child += 1;
}
if(arr[child] > arr[parent]){
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
}else{
break;
}
}
return;
}
// 下沉式调整创建堆
void CreateHeapByDown(int arr[], int arr_size)
{
// 最后一个元素的下标是 size-1 ,它的父节点下标就是 (arr_size - 1 - 1) / 2
// 从最后一个父节点开始创建
int index = (arr_size - 1 - 1) / 2;
for(; index >= 0; index --){
// 从最后一个父节点开始下沉,符合规则就进行下沉,然后更新父子节点下边(向上更新),然后继续比较
// 直到父节点为根节点进行比较后,结束循环
AdjustDown(arr, arr_size, index);
}
return;
}
堆排序需要借助一个堆来完成,大堆中,父节点的值要大于子节点,小堆中,父节点的值要小于子节点,给定义一个数组,来构建一个堆。
(1)若要进行升序排序,则需要创建一个大堆,对创建好的大堆进行删除堆顶元素,直接删除的话,需要搬运数组元素,可以交换堆顶和最后一个节点,然后 -- 堆的结点个数,这样操作的话,堆的有效元素就不包括最后一个数组元素了,刚好最后一个元素是数组中的最大值。删除后,需要对堆顶元素进行下沉调整,对比左右子树的值大小,堆顶元素与较大的结点值进行交换(借助 child 和 parent 记录下标,第一次下沉操作parent 指向堆顶元素,child指向其孩子结点,交换后继而更新两个下标,直到 堆顶元素移动到符合堆规则位置)(2)若要进行降序排序,需要创建一个小堆,对创建好的小堆也进行删除堆顶元素操作,先交换堆顶元素和最后一个堆结点的位置,再对新的堆顶点进行下沉操作。
// 删除堆顶元素
void DeleteHeapTop(int arr[], int arr_size)
{
int index = arr_size - 1;
for(; index > 0; index --){
Swap(&arr[0], &arr[index]);
AdjustDown(arr, index, 0);
}
}
// 堆排序 升序
void HeapSort(int arr[], int arr_size)
{
if(arr == NULL || arr_size <= 1){
return;
}
CreateHeapByUp(arr, arr_size);
DeleteHeapTop(arr, arr_size);
return;
}
5.希尔排序:
时间复杂度:O(n^2) 最好的情况是 O(n^1.3)
空间复杂度:O(1)
稳定性:不稳定
算法思路:
希尔排序是进阶版的插入排序,鉴于插入排序的特点,希尔排序采取分组插入排序,先将待排序序列进行分组,以步长( gap ,初始值一般设为 序列元素总个数的 1/2)来区分,0,0+gap,0+2*gap... 为一组 1,1+gap,1+2*gap为一组... 等等,相距 gap 个元素的为一组,对每一组的元素进行直接插入排序,然后减小步长(gap= gap / 2),再进行新的小组内排序,直到步长为 1 ,即对序列进行直接插入排序,这个时候序列已经基本有序,效率比较快。
每个小组内排序,和插入排序一样,用 bound 来作为小组的边界,cur 和 bound 保持一致来辅助完成排序,bound 开始指向第一个小组的第二个元素,以后向后++,依次再指向第二组、第三组...的第二个元素,走过一个 gap 的步数,就依次到每个小组的第三个元素,以此类推,直到遍历完序列。bound移动一次, cur 要给 bound 元素在当前小组内前面找到一个合适的位置,找到以后,bound++,继续找下一个,用 cur-gap 来标记 cur当前小组的前一个元素。
希尔排序的效率取决于步长。
// 希尔排序 升序
void ShellSort(int arr[], int size)
{
if(arr == NULL || size <= 1){
return;
}
// 初始步长定位 size / 2
int gap = size / 2;
for(; gap >= 1; gap = gap / 2){
// gap = 1 时,就等于对数组进行直接插入排序,这时候数组已经基本有序了,效率快
// gap 每次减少 1/2
int bound = gap;
for(; bound < size; bound ++){
// bound 从第一组的第二个元素开始遍历,直到数组结束,cur辅助来对小组内的元素排序
int cur = bound;
int bound_value = arr[bound];
for(; cur >= gap; cur -= gap){
// cur 从后往前比较 元素大小,进行交换
if(arr[cur - gap] > bound_value){
arr[cur] = arr[cur - gap];
}else{
break;
}
}
// 填补空位
arr[cur] = bound_value;
}
}
}
6.归并排序:
时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定
算法思路:
(1)递归方法:
利用递归的思想,将带排序序列进行拆分,递归的拆分成更小的区间,直到区间只剩下一个元素时,不需要排序,已经有序了,然后需要将结果归并起来,创建一个临时数组来保存归并的结果,最后将临时数组的值赋给原数组。以下图为例,待排序序列为 {4,3,2,5,6,1},分成橙色划分区间,继续划分成黄色区间,再继续划分成绿色区间,此时绿色区间已经有序,将结果按序归并到一起,然后向上归并黄色区间,再继续向上归并橙色区间,得到的结果就是有序的了。
下图为划分归并的详细过程:
// 归并数组
void MergeArr(int arr[], int left, int mid, int right, int *tmp)
{
// 遍历区间,将有效的子区间用tmp数组保存起来
int left_index = left;
int right_index = mid;
int tmp_index = left;
while(left_index < mid && right_index < right){
// 左右区间都有元素的时候,进行比较
if(arr[left_index] < arr[right_index]){
tmp[tmp_index ++] = arr[left_index ++];
}
else{
tmp[tmp_index ++] = arr[right_index ++];
}
}
// 一个区间的元素遍历完了,直接将另一个区间的剩余元素加到 tmp 数组中
while(left_index < mid){
tmp[tmp_index ++] = arr[left_index ++];
}
while(right_index < right){
tmp[tmp_index ++] = arr[right_index ++];
}
// 将 tmp 的元素搬运到 原数组中
int i = left;
for(; i < right; ++i){
arr[i] = tmp[i];
}
return;
}
// 辅助递归函数
void _MergeSort(int arr[], int left, int right, int* tmp){
if(right - left <= 1){
return;
}
int mid = left + (right - left) / 2;
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid, right, tmp);
// 每一个小区间进行排序后,修改在数组中的顺序
MergeArr(arr, left, mid, right, tmp);
return;
}
// 递归归并排序 升序
void MergeSort(int arr[], int size)
{
if(arr == NULL || size <= 1){
return;
}
// 用临时数组来保存归并时的有序序列
int* tmp = (int*)malloc(sizeof(int)*size);
_MergeSort(arr, 0, size, tmp);
free(tmp);
return;
}
(2)非递归方法:
非递归进行归并排序还是采用分区间的思想,将大区间划分成小区间,然后再进行归并,使用双重循环,第一重循环用来分区间,第二重循环用来进行归并。
用 gap 来进行分区间,gap 表示区间的元素个数,gap = 1,2,4,8……到数组的元素个数,还是使用三个下标来辅助完成归并,left,right,mid,[left, mid)表示左区间,[mid,right)表示右区间来进行归并,分区间归并后的有序序列存到临时数组 tmp 中方便保存,每次归并从第一个元素开始,用 index 来表示数组下标,index 每循环一次 增加 2*gap,因为一个区间的元素为 gap 个,每次对左右两个区间合并,所以需要跨越两个区间的元素。
left = index,mid = index + gap, right = index + 2*gap ,注意在移动过程中,mid、right 可能超出数组范围,这样就没法归并最后一个区间,需要将mid、right设为合法值,让其都等于left。
gap 逐渐增大,当归并的区间是整个数组即 gap = size 时,就完成了排序。下图为详细过程:
// 非递归归并排序 升序
void MergeSortByLoop(int arr[], int size)
{
if(arr == NULL || size <= 1){
// 非法输入或者小于两个元素,不需要排序
return;
}
int *tmp = (int*)malloc(sizeof(int)*size);
// 用步长来分区间的大小
int gap = 1;
for(; gap < size; gap = gap*2){
int index = 0;
for(; index < size; index = index + 2*gap){
int left = index;
int right = index + 2*gap;
int mid = index + gap;
if(mid >= size){
// 防止合并最后一个区间的时候发生越界
mid = left;
}
if(right >= size){
// 防止合并最后一个区间的时候发生越界
right = size;
}
MergeArr(arr, left, mid, right, tmp);
}
}
free(tmp);
}
7.快速排序:
时间复杂度:O(nlogn) 最坏的情况是 O(N^2) 有两种情况:1.数组完全是反序 2.数组的长度很长
空间复杂度:O(n) 最坏情况
稳定性:不稳定
算法思路:
1)递归方法:
(1)交换法:以升序为例,在待排序序列中选取一个基准值(一般选取第一个元素或者最后一个元素),begin 下标从第一个元素向后走,end 下标从最后一个元素向前走,begin 遇到比基准值大的就停下来,end 遇到比基准值小的就停下来。
( 基准值是最后一个,begin 先移动;基准值是第一个,end 先移动。)
交换 begin 和 end 下标所在的值,继续移动 begin 、end ,直到 begin = end ,将 begin 和 end 所在的元素与基准值交换,此时,在基准值之前的元素都小于基准值,之后的元素大于基准值,返回 begin/end 。
返回的下标作为中间下标,继续分别对左右子区间递归进行以上操作,子区间只有一个元素时,直接返回所在下标,当递归结束的时候,排序就完成了。
// 递归版本1,交换法
int MethodSwap(int arr[], int left, int right)
{
if(right - left <= 1){
return left;
}
int standard = arr[right - 1];
int begin = left;
int end = right - 1;
while(begin < end){
// 以 第一个元素为基准的话,end 先走
// 以 最后一个元素为基准的话,begin先走
while(begin < end && arr[begin] <= standard){
++begin;
}
while(end > begin && arr[end] >= standard){
--end;
}
Swap(&arr[begin], &arr[end]);
}
Swap(&arr[end], &arr[right - 1]);
return end;
}
(2)挖坑法:以升序为例,在待排序序列中选择一个位置作为“坑”,将坑的值保存起来,begin 下标从头开始,end 下标从尾开始,(选择第一个元素作为坑的,end先开始移动;选择最后一个元素,begin先开始移动)
当 begin 遇到一个比坑值大的元素,就将 begin 位置的元素填到旧坑里(即 end 的位置),这时候 begin 位置就是一个新坑
当 end 遇到一个比坑值小的元素,就将 end 位置的元素填到旧坑里(即 begin 的位置),这时候 end 位置就是一个新坑
继续移动 begin、end 直到 begin = end 返回 begin/end ,递归的对子区间进行以上操作。当子区间只有一个元素的时候直接返回,递归退出,排序就完成了。
// 递归版本2,挖坑法
int MethodPit(int arr[], int left, int right)
{
if(right - left <= 1){
return left;
}
// 将坑的元素保存起来,然后进行比较
int pit = arr[right - 1];
int begin = left;
int end = right - 1;
while(begin < end){
while(begin < end && arr[begin] <= pit){
++begin;
}
// 判断是否已经出现新坑
if(arr[begin] > pit){
// 需要将新坑的值赋给旧坑,begin 处是新坑
arr[end] = arr[begin];
}
while(end > begin && arr[end] >= pit){
--end;
}
// 判断是否有新坑
if(arr[end] < pit){
// 需要将新坑的值保存下来,end 处是新坑
arr[begin] = arr[end];
}
}
arr[end] = pit;
return end;
}
// 辅助递归函数
void _QuickSort1(int arr[], int left, int right)
{
if(arr == NULL || right - left <= 1){
return;
}
// int mid = MethodSwap(arr, left, right);
int mid = MethodPit(arr, left, right);
_QuickSort1(arr, left, mid);
_QuickSort1(arr, mid + 1, right);
return;
}
// 递归快速排序 升序
void QuickSort1(int arr[], int size)
{
if(arr == NULL || size <= 1){
return;
}
_QuickSort1(arr, 0, size);
return;
}
2)非递归方法:
处理局部的时候,需要将未处理的部分保存记录,借助栈来完成,栈保存数组的下标位置,先入栈数组的首尾下标。去栈顶元素,如果栈为空的话,说明排序完成;
取出要处理的区间的首尾坐标,对区间进行交换法或者挖坑法排序,其返回值将区间 再细分成 [left,返回值) ,[返回值,right) 两个区间;
分别按顺序入栈这两个区间的首尾坐标,继续进行下一次循环,如果区间只有一个元素,直接进入下一次循环;
栈为空时,序列就是有序的了。
// 循环实现快速排序 升序
void QuickSortByLoop(int arr[], int size)
{
if(arr == NULL || size <= 1){
return;
}
// 利用栈来完成
SeqStack s;
SeqStackInit(&s);
// 先入栈 0 和 最后一个下标
SeqStackPush(&s, 0);
SeqStackPush(&s, size);
int left = 0;
int right = 0;
while(1){
// 先取出要处理区间的尾坐标
int ret = SeqStackTop(&s, &right);
if(ret <= 0){
// 栈为空说明,已经全部排序完成
return;
}
SeqStackPop(&s);
// 再取出要处理区间的首坐标
SeqStackTop(&s, &left);
SeqStackPop(&s);
if(right - left <= 1){
// 如果区间只有一个元素,直接返回
continue;
}
// 找划分区间的中间元素
int mid = MethodPit(arr, left, right);
// 先入栈新右区间的首坐标,尾坐标
SeqStackPush(&s, mid);
SeqStackPush(&s, right);
// 再入栈新左区间的首位坐标
SeqStackPush(&s, left);
SeqStackPush(&s, mid);
}
return;
}
3)快速排序的改进思想:
(1)修改选取基值的方法 :从三个元素中选中,开始,中间,最后
(2)当递归深度一定的时候,不在递归,采取堆排序
(3)当递归到一定程度时,子区间的元素个数较少,使用插入排序
8.其他排序:
(1)计数排序:
利用哈希的思想来进行排序,创建一个数组,将序列中元素出现的次数值在对应的数组下标处,统计相同元素出现的次数,再进行回收到原来的序列,所有元素都遍历完,序列就有序了,只能用于对正整数进行排序。
(2)基数排序:
(2)基数排序:
类似于哈希桶的思想,来进行排序。将序列元素进行分类来完成排序,按照关键字来划分。
(3)睡眠排序:
时间复杂度O(0)
创建与多个线程,每个线程内设定睡眠函数,睡眠函数的值等于序列元素的值,等程序运行结束,序列就有效了。
(4)猴子排序:
时间复杂度 O(无穷)
随机对数组进行排列