前言
- 本篇博客主要介绍交换排序算法中的冒泡排序(BubbleSort)和快速排序(QuickSort),包括他们的思路和实现,另着重介绍快速排序的三种实现思路以及两个方面的优化。
- 代码实现:C语言
- 交换排序基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
- 交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
导航🗺🌎🏁:
目录
冒泡排序
✨基本思想:
对待排序列进行遍历,左右两个数进行比较,左边大于右边则进行交换(升序),继续比较下一对数据,完全一次遍历后,最大的数冒到了最右边,然后再继续遍历,执行上面步骤。
🍋冒泡排序应该是比较简单的排序算法
🎆🎆实现步骤:
前提:给定长度为N的待排序列,要求排升序
- 遍历待排序列[0, N-1],比较左右数据大小,左边大于右边则进行交换
- 继续遍历序列[0, N-2],重复操作,把次大的数冒到右边
- ……
下面引用一张gif进行演示:
🎉🎉🎉代码如下:
优化步骤: 设置一个标志位exchange,在外重循环赋初值0,在内层循环,遍历一趟若有交换操作,则赋值1,否则不改变exchange的值。如此操作一次后对exchange进行判断,若无改变则说明序列已经有序,结束算法。void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } void BubbleSort(int* a, int n) { assert(a); for (int i = 0; i < n; i++) { int exchange = 0; for (int j = 1; j < n - i; j++) { if (a[j - 1] > a[j])//大于排升序 小于排降序 { Swap(&a[j - 1], &a[j]); exchange = 1; } } if (exchange == 0) { break; } } }
🔔🔔🔔🔔特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2),最好情况O(N)
- 空间复杂度:O(1)
- 稳定性:稳定
快速排序
✨基本思想:
📖快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。运用了分治的思想。
🎆🎆基本递归框架:
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像, 在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式(即PartSort函数)即可。// 假设按照升序对a数组中[begin, end]区间中的元素进行排序 void QuickSort(int* a, int begin, int end) { assert(a); if (begin >= end) { return; } // 按照基准值对array数组的 [begin, end]区间中的元素进行划分 //int keyi = PartSort1(a, begin, end); //int keyi = PartSort2(a, begin, end); int keyi = PartSort3(a, begin, end); // 划分成功后以keyi为边界形成了左右两部分 [begin, keyi - 1] 和 [keyi + 1, end] // 递归排[begin, keyi - 1] QuickSort(a, begin, keyi - 1); // 递归排[keyi + 1, end] QuickSort(a, keyi + 1, end); }
🎉🎉🎉PartSort函数:
将区间按照基准值划分为左右两半部分的方法,这里定义成PartSort函数,主要有以下三种实现思路:
🥕Hoare版本:PartSort1
gif动图:
流程如下:
为什么key选左边要右边先走,下面进行分析:
实现代码:
void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } //Hoare int PartSort1(int* a, int begin, int end) { int left = begin, right = end; int keyi = left; while (left < right) { //右边先走,找小 while (left < right && a[right] >= a[keyi]) { right--; } //左边再走,找大 while (left < right && a[left] <= a[keyi]) { left++; } Swap(&a[left], &a[right]); } Swap(&a[keyi], &a[left]); keyi = left; return keyi; }
🥕🥕挖坑法:PartSort2
gif动图:
流程如下:
实现代码:
//挖坑法 int PartSort2(int* a, int begin, int end) { int key = a[begin]; int hole = begin; while (begin < end) { //右边找小,填到左边的坑里,这个位置形成新的坑 while (begin < end && a[end] >= key) { end--; } a[hole] = a[end]; hole = end; //左边找小,填到右边的坑里,这个位置形成新的坑 while (begin < end && a[begin] <= key) { begin++; } a[hole] = a[begin]; hole = begin; } a[hole] = key; return hole; }
🥕🥕🥕前后指针版本:PartSort3(容易理解和操作,推荐使用)
gif动图:
流程图:
实现代码:
void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } //前后指针法 int PartSort3(int* a, int begin, int end) { int prev = begin; int cur = begin + 1; int keyi = begin; while (cur <= end) { // cur位置的值小于keyi位置的值 if (a[cur] < a[keyi] && ++prev != cur) { Swap(&a[cur], &a[prev]); } cur++; } Swap(&a[prev], &a[keyi]); keyi = prev; return keyi; }
快速排序的优化
✨优化方向:
主要是考虑到key的值若每次都固定取最左边,若出现逆序情况(即最坏情况),那么PartSort函数其实意义不大;此外考虑到递归区间若小于10~20,或者说递归深度太深,可能会出现栈溢出。所以快排的优化主要基于这两个方面进行:
- 三数取中法选key:在递归区间[begin, end]中,比较下标begin、mid = (begin + end) / 2、end的值,取中间的数作为key
- 减少递归次数:递归到小的子区间时,使用直接插入排序
🎆🎆优化代码:
🍂优化1:三数取中法选key
int GetMidIndex(int* a, int begin, int end) { int mid = (begin + end) / 2; if (a[begin] < a[mid]) { if (a[mid] < a[end]) { return mid; } else if (a[begin] < a[end]) { return end; } else { return begin; } } else// a[begin] > a[mid] (可以认为不用管相等的情况) { if (a[mid] > a[end]) { return mid; } else if (a[begin] < a[end]) { return begin; } else { return end; } } }
🍂🍂优化2:减少递归次数,对小的子区间进行直接插入排序
下面以PartSort3为例,加入优化1和优化2的快速排序代码如下:
void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } //优化1 - 加入三数取中 int GetMidIndex(int* a, int begin, int end) { int mid = (begin + end) / 2; if (a[begin] < a[mid]) { if (a[mid] < a[end]) { return mid; } else if (a[begin] < a[end]) { return end; } else { return begin; } } else// a[begin] > a[mid] (可以认为不用管相等的情况) { if (a[mid] > a[end]) { return mid; } else if (a[begin] < a[end]) { return begin; } else { return end; } } } //前后指针法 int PartSort3(int* a, int begin, int end) { int prev = begin; int cur = begin + 1; int keyi = begin; //优化操作1 - 三数取中 int midi = GetMidIndex(a, begin, end); Swap(&a[begin], &a[midi]); while (cur <= end) { // cur位置的值小于keyi位置的值 if (a[cur] < a[keyi] && ++prev != cur) { Swap(&a[cur], &a[prev]); } cur++; } Swap(&a[prev], &a[keyi]); keyi = prev; return keyi; } //优化操作2 - 减少递归调用次数 void QuickSort(int* a, int begin, int end) { assert(a); if (begin >= end) { return; } //减少递归调用次数 if (end - begin > 10) { int keyi = PartSort3(a, begin, end); //递归 QuickSort(a, begin, keyi - 1); QuickSort(a, keyi + 1, end); } else//调用插入排序 { InsertSort(a + begin, end - begin + 1); } }
快速排序递归改非递归(较难,要求掌握)
✨基本思想:
利用数据结构栈后进先出的特性,模拟递归过程
🎆🎆栈:
由于C语言并不像C++那样能直接调用栈,所以这里得先提前用C语言把栈实现一下。
关于栈的实现更加详细的知识:💫💫➡传送门
这里直接给出相关代码:Stack.h和Stack.c
🥕Stack.h
#pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> //静态的栈 定长,实际中一般不实用 //typedef int STDateType; //typedef struct Stack //{ // STDateType a[N];//N的值要给定 // int top; //}ST; //动态的栈 支持动态增长 typedef int STDateType; typedef struct Stack { STDateType* a; int top;//栈顶 int capacity;//容量 }ST; //初始化栈 void StackInit(ST* ps); //销毁栈 void StackDestroy(ST* ps); //入栈 void StackPush(ST* ps, STDateType x); //出栈 void StackPop(ST* ps); //获取栈顶元素 STDateType StackTop(ST* ps); //检测栈是否为空 bool StackEmpty(ST* ps); //获取栈的长度(栈中存放数据的个数) int StackSize(ST* ps);
🥕Stack.c
#define _CRT_SECURE_NO_WARNINGS 1 #pragma warning(disable:6031) #include "Stack.h" void StackInit(ST* ps) { assert(ps); ps->a = NULL; ps->top = 0; ps->capacity = 0; } void StackDestroy(ST* ps) { assert(ps); free(ps->a); ps->a = NULL; ps->top = 0; ps->capacity = 0; } void StackPush(ST* ps, STDateType x) { assert(ps); if (ps->top == ps->capacity) { int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; STDateType* tmp = (STDateType*)realloc(ps->a, sizeof(STDateType) * newCapacity); if (tmp == NULL) { printf("realloc fail\n"); exit(-1); } ps->a = tmp; ps->capacity = newCapacity; } ps->a[ps->top] = x; ps->top++; } void StackPop(ST* ps) { assert(ps); assert(!StackEmpty(ps)); ps->top--; } STDateType StackTop(ST* ps) { assert(ps); assert(!StackEmpty(ps)); return ps->a[ps->top - 1]; } bool StackEmpty(ST* ps) { assert(ps); return ps->top == 0; } int StackSize(ST* ps) { return ps->top; }
🎉🎉🎉非递归代码如下:
优化1 - 加入三数取中 int GetMidIndex(int* a, int begin, int end) { int mid = (begin + end) / 2; if (a[begin] < a[mid]) { if (a[mid] < a[end]) { return mid; } else if (a[begin] < a[end]) { return end; } else { return begin; } } else// a[begin] > a[mid] (可以认为不用管相等的情况) { if (a[mid] > a[end]) { return mid; } else if (a[begin] < a[end]) { return begin; } else { return end; } } } //前后指针法 int PartSort3(int* a, int begin, int end) { int prev = begin; int cur = begin + 1; int keyi = begin; //优化操作1 - 三数取中 int midi = GetMidIndex(a, begin, end); Swap(&a[begin], &a[midi]); while (cur <= end) { // cur位置的值小于keyi位置的值 if (a[cur] < a[keyi] && ++prev != cur) { Swap(&a[cur], &a[prev]); } cur++; } Swap(&a[prev], &a[keyi]); keyi = prev; return keyi; } void QuickSortNonR(int* a, int begin, int end) { ST st; StackInit(&st); StackPush(&st, end); StackPush(&st, begin); while (!StackEmpty(&st)) { int left = StackTop(&st); StackPop(&st); int right = StackTop(&st); StackPop(&st); int keyi = PartSort3(a, left, right); //[left, keyi - 1] keyi [keyi + 1, right] if (keyi + 1 < right) { StackPush(&st, right); StackPush(&st, keyi + 1); } if (left < keyi - 1) { StackPush(&st, keyi - 1); StackPush(&st, left); } } StackDestroy(&st); }
🔔🔔🔔🔔特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
学习记录:
- 📆本篇博客整理于2022.7.10
- 🎓作者:如何写出最优雅的代码
- 📑如有错误,敬请指正🌹🌹
- 🥂如果觉得写的不错,看完了别忘了点赞和收藏啊,感谢支持😏😏