基本概念
定义
重新排列表中的元素,使其满足元素按照关键字有序的过程
排序算法稳定性:假设待排序表中,两个元素R1和R2,对应关键字Key1==Key2,在使用某种排序算法后,仍保持原先的相对顺序,则该排序算法稳定;否则不稳定。(算法是否稳定并不衡量排序算法的优劣,只是描述性质
根据数据元素是否完全在内存中,可以将排序算法分为内部排序和外部排序
并非所有排序都要基于元素之间的比较,例如基数排序
例题
对n个数进行基于比较的排序,需要进行至少「log(2, n!) 次关键字之间的比较
插入排序
基本思想:将每一个待排序元素插入前面已排序好的子序列,直到排序完成。
可以引申出:直接插入排序、折半插入排序和希尔排序
直接插入排序
算法流程:
- 找出L(i)在L[1…i-1]中的插入位置
- 进行插入
代码:
1 | // 逐个交换版本 |
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
适用性:适用于顺序存储和链式存储的线性表
折半插入排序
在查找插入位置的时候使用折半查找进行优化
1 | void Insert_Sort3(int nums[], int len){ |
时间O(n^2)
空间O(1)
稳定
希尔排序
希尔排序,又称缩小增量排序。
把相隔d增量的元素记录为一个子表,在各组内进行直接插入排序。不断缩小步长,直到步长为1,此时直接插入排序。因为有较好的局部有序性,所有可以很快得到结果
1 | void Shell_Sort(int nums[], int len){ |
时间复杂度:较难分析,n在某个特定区间内为O(n^1.3),最坏时间复杂度为O(n^2)
空间复杂度:O(1)
不稳定
适用性:仅适用于线性表为顺序存储的情况
交换排序
包括冒泡排序和快速排序
冒泡排序
基本思想:从后往前(或者从前往后)两两比较相邻元素的值,若为逆序,则交换他们,知道序列比较完,称为一次冒泡,结果是最小的元素交换到待排序序列的第一个位置
1 | void Bubble_Sort(int nums[], int len){ |
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定
快速排序
思想:基于分治。
- 在待排序表中任意选取一个元素pivot,通过一趟排序,将待排序表分为独立的两部分L和R,L中所有元素小于pivot,R中所有元素大于pivot,
- 此时pivot位于有序数组的最终位置上
- 递归处理L和R。直到每部分内都只有一个元素
Partition过程
Partition将待排序表划分。快速排序的性能主要取决于此。
此处以《数据结构》王蔚敏版本的做法,以表中第一个元素作为枢轴pivot,进行划分
1 | int partition(int nums[], int left, int right){ |
类似荷兰国旗问题的partition
1 | int partition(int nums[], int left, int right){ |
递归处理部分
1 | void QuickSort(int nums[], int left, int right){ |
空间复杂度:O(log(2, n)) 递归需要用栈
时间复杂度:O(log(2, n))
稳定性:不稳定
习题
- 2/ 编写双向冒泡排序算法,在正反两个方向交替进行扫描:即第一趟把最大的关键字放在序列最后,第二趟把最小的关键字放在最前,如此往复
思路:用一个times变量控制扫描方向
1 | void _02_BiDirection_bubble_sort(int nums[], int len){ |
- 3/ 线性表按顺序存储,每个元素是都不相同的整形,要求将所有奇数移动到所有偶数前面
思路:荷兰国旗问题的变种,将所有奇数移动就可以
1 | void _03_func(int nums[], int len){ |
- 5/ 在数组中找出第k小的元素
思路:先快排,然后直接取出
1 | // partition |
- 6/ 荷兰国旗问题:设有一个仅有红白蓝三种颜色的条块组成的序列,编写算法使序列按照红、白、蓝的顺序排好,即排成荷兰国旗图案
思路:
- 假设0代表红,1代表白,2代表蓝。
- 只要将0和2排到数组前面和后面,就完成题意
- 设置left表示左边排好序的边界,right表示右边的边界。进行一遍扫描,交换
1 | void _05_Holland_Flag(int nums[], int len){ |
- 6/ (2016统考真题)已知n(n>=2)个正整数构成的集合A,将其划分为两个不相交子集A1和A2,元素个数为n1和n2,子集内元素和为S1和S2,设计一个高效的划分算法,满足|n1-n2|最小并且|S1-S2|最大
思路:贪心思想,|n1-n2|最小,要求平分;|S1-S2|最大,要求A1内元素都小,A2内元素都大。因此,类似快排,选取一个枢轴,他应当排序后位于数组中间
- 选取一个枢轴 将其放到正确位置
- 正确位置为len/2-1。
- 如果枢轴位置刚好等于len/2-1,那就计算答案
- 如果小于len/2-1,则划分右半边;否则划分左半边,更改边界即可
1 | int _06_func(int nums[], int len){ |
选择排序
基本思想:每一趟排序,在后面的待排序序列中找到一个最小值,放到排好序序列的后面。
简单选择排序
每一趟在L[i…n]中找到最小值,和L(i)进行交换
1 | void Select_Sort(int nums[], int len){ |
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
堆排序
堆的定义
n个关键字序列L[1…n]满足以下条件可以成为堆
- L(i)>=L(2i) && L(i)>=L(2i+1) 或者
- L(i)<=L(2i) && L(i)<=L(2i+1)
满足条件1称为大根(顶)堆,满足条件2称为小根(顶)堆
堆可以视为一棵完全二叉树
以大顶堆实现为例,排序结果为从小到大。区别于王道考研书,这里数组下标从0开始计算
堆化
堆化是自上而下的
对于每一个节点,比较自己和孩子中较大的那个,不断下沉
1 | void heapify(int nums[], int len, int k){ |
建立堆
建立堆是自下而上的
对于每一个节点,比较自己和父节点,不满足堆的性质则进行交换
1 | void BuildHeap(int nums[], int len){ |
堆排序
堆排序则是:
- 建立堆
- 每次取出堆顶元素(待排序序列中的最大值),与后面排序完成的序列前一位进行交换。然后对新的堆顶元素堆化
- 当堆只有一个元素时,堆排序完成
1 | void Heap_Sort(int nums[], int len){ |
时间复杂度:O(n*log(2, n))
空间复杂度:O(1)
稳定性:不稳定
堆排序适用于关键字较多的情况,例如10000个数据中取最小的100个,可以用大小为100的堆实现
归并排序和基数排序
归并排序
不同于之前的基于比较、选择等排序,归并排序的基本思想是:将两个或两个以上的有序表组合成一个新的有序表。
Merge 操作
Merge 将两个表合并成一个有序表,需要借助一个辅助数组来完成。双指针
1 | void Merge(int nums[], int left, int mid, int right){ |
MergeSort 操作
MergeSort递归做法,类似树的后序遍历。
1 | void MergeSort(int nums[], int left, int right){ |
时间复杂度:O(n*log(2, n))
空间复杂度:O(n) 来自辅助数组和递归函数栈
稳定性:稳定