概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
蛮力
简单选择排序
- 思想:每一趟扫描后面n-i+1个待排序元素,选取其中的最小元素,作为有序子序列的第i个元素,直到第n-1趟结束
void SelectSort(int A[], int n){
for(int i=0; i<n-1; i++){
int minpos = i; // 记录最小元素位置,最终放到i处
for(int j=i+1; j<n; j++){
if(A[j]<A[minpos]) minpos = j;
}
if(minpos!=i) swap(A[i], A[minpos]); // 与第i个元素交换位置
}
}
- 时间效率:最好的就是已经排好序的,移动0次,需要元素比较n(n-1)/2次,所以时间复杂度为O(n2)。
冒泡排序
- 思想:两两比较相邻元素的值,若为逆序(A[i-1]>A[i] 若想升序排列),则交换它们,直到序列比较完,完成一趟冒泡,把序列中最小的放到了待排序列的前面,下一次冒泡时前面已经确定的最小元素不参与。最多n-1次冒泡就能排好。
void BubbleSort(int A[], int n){// 升序
for(int i = 0; i < n-1; i++){ // n-1趟排序就OK
bool flag = false; // 本次排序是否发生交换
for(int j = n-1; j>i; j--){ // 一趟冒泡排序
if(A[j-1]>A[j]){
swap(A[j-1],A[j]);
flag = true;
}
if(flag==false) return; // 本次遍历没有发生交换,说明已经OK了
}
}
}
- 空间效率:只用了常数个辅助单元O(1)
- 时间效率:
- 最好是已经完全排序,比较n-1次,移动0次。
- 最坏是完全逆序,n-1趟排序,第i趟需要n-i次关键字比较,每次比较移动3次来交换元素位置。O(n2).
- 平均O(n2)
减治法
插入排序
- 思路:取出未排序的数列第一个数,插入已排序的数列中的恰当位置,
再从余下来未排序的数列中的第一个数取出,放入。 - 时间:全升序是最好的:O(n);全降序是最差的:O(n2),平均O(n2)
- 适用于顺序存储和链式存储的线性表
// 直接插入排序
void InsertSort(int* h, size_t len){
if(h==NULL) return;
if(len<=1) return;
int i,j;
//i是次数,也即排好的个数;j是继续排
for(i=1;i<len;++i)
for(j=i;j>0;--j) // h[j]元素去前面找它该去的位置
if(h[j]<h[j-1]) swap(h[j],h[j-1]);
else break;
return;
}
希尔排序
- 思路:跳跃式分组,对每个组做插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。
- 在特定范围内,时间复杂度为O(n1.3),最坏情况下为O(n2)。
- 不稳定,相同关键字分到不同子表,可能会改变它们之间的相对次序。
- 希尔排序只适用于线性表为顺序存储的情况。
//希尔排序
void ShellSort(int* h, size_t len){
if(h==NULL) return;
if(len<=1) return;
for(int div=len/2;div>=1;div/=2) // 步长div变化
for(int k=0;k<div;++k) // 形成了div个分组,对每一组做如下操作:
for(int i=div+k;i<len;i+=div) // 对一个分组进行插入排序
for(int j=i;j>k;j-=div) // 去这个分组里前面找它的位置
if(h[j]<h[j-div]) swap(h[j],h[j-div]);
else break;
return;
}
拓扑排序
- 拓扑排序是个有向无环图,每个顶点出现且只出现1次,若A→B,则序列中A在前,B在后。只有DAG才有拓扑排序。
- DFS:执行DFS遍历,写入堆栈中;然后出栈顺序即逆序;边遇到回边,不返回
- 减治技术:找出源(没有输入边的顶点)→把它和所有从它出发的点删除→顶点被删的次数就是解。
分治法
快排
-
思想: left指针,right指针,key参照数(分水岭)
- 当左指针位置 < 右指针:
- 右指针从右往左遍历,遇到小的就赋值给left。
- 而后左指针向右遍历,遇到大的就赋值给right。而后右指针左遍历同理。
- 最终左右指针重叠:让key左边的都<她,右边的都>她
- 然后递归左右两边。
- 当左指针位置 < 右指针:
-
过程:以20 40 50 10 60 为例
- (1)从left取出数字 (20) 作为基准参照物。
- (2)从数组right位置向前找,找到< key的数,则将该数赋值给left,此时数组为10 40 50 10 60,左右指针指向10
- (3)从left向后找,一直找到比(key)大的数,如果找到,将此数赋给right的位置(也就是40赋给10),此时数组为:10,40,50,40,60, left和right指针分别为前后的40。
- (4)重复2,3步骤直到俩指针重合,最后将(key)放到40的位置, 此时数组值为: 10,20,50,40,60,至此完成一次排序。
- (5)此时20已经潜入到数组的内部,20的左侧一组数都比20小,20的右侧作为一组数都比20大, 以20为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。
void QuickSort(int A[], int left, int right){
if(left < right){
int pivotpos = Partition(A, left, right); // key的位置为分水岭
QuickSort(A, left, pivotpos-1); // 递归
QuickSort(A, pivotpos+1, right);
}
}
int Partition(int A[], int left, int right){
int key = A[left]; // 设定当前数组最左元素为基准元素
while(left < right){
while(left<right && A[right]>=key) --right;
if(left < right) A[left] = A[right];
while(left<right && A[left]<key) ++left;
if(left < right) A[right] = A[left];
}
A[left] = key; // 把基准元素放到它最终位置
return left; // 返回基准元素的位置,用来划分下面的两个子数组
}
- 不稳定算法:右端区间有俩值一样,且都<key,那么他俩在交换到左边后,相对位置是变化了的。
- 空间效率:递归工作栈,容量和递归调用的最大深度一致,
- 最好 ⌈ log 2 ( n + 1 ) ⌉ \left\lceil \log _{ 2 }{ \left( n+1 \right) } \right\rceil ⌈log2(n+1)⌉,
- 最坏要进行n-1次递归,栈的深度是O(n),
- 平均,深度是 O ( log 2 n ) O\left( \log _{ 2 }{ n } \right) O(log2n)
- 时间效率:快排的运行时间和划分是否对称有关:
- 最坏:两边n-1个和0个,即初始排序基本有序或逆序, O ( n 2 ) O\left( { n }^{ 2 } \right) O(n2),比较次数 n − 1 + n − 2 + … + 1 = n ( n − 1 ) 2 ∼ n 2 2 n-1 + n-2 +…+1 =\frac { n\left( n-1 \right) }{ 2 } \sim \frac { { n }^{ 2 } }{ 2 } n−1+n−2+…+1=2n(n−1)∼2n2
- 最好:每次都正好中分 T ( n ) = O ( n log 2 n ) T\left( n \right) =O\left( { n }\log _{ 2 }{ n } \right) T(n)=O(nlog2n),一次递归比较n次,递归深度 l o g 2 n log_{2}{n} log2n
- 平均: 推导过程
注:ln( n ) > 1/2 + 1/3 … + 1/n
- 若是随机选取起点,则代码如下:
// 快速排序,随机选取哨兵放前面
void QuickSort(int* h, int left, int right)
{
if(h==NULL) return;
if(left>=right) return;
//防止有序队列导致快速排序效率降低
srand((unsigned)time(NULL));
int len=right-left;
int kindex=rand()%(len+1)+left;
Swap(h[left],h[kindex]);
// 也可以直接用数组的最左值作为基准点
int key=h[left],i=left,j=right;
while(i<j) // 左指针位置<右指针则可以继续这趟遍历
{
while(h[j]>=key&&i<j) --j; // 若右边的值大于key,则右指针持续向左遍历
if(i<j) h[i]=h[j]; // 当遇到小的了,赋值给左指针指向的位置
while(h[i]<key&&i<j) ++i;// 若左边的数小,左指针向右遍历
if(i<j) h[j]=h[i]; // 当遇到大的了,就赋值给右边指向的位置
}
// 俩指针重叠后,此轮遍历结束,找到key的最终位置
h[i]=key;
// 继续对两边的子数组同理操作(递归)
QuickSort(h,left,i-1);
QuickSort(h,j+1,right);
}
归并排序
- 思路:先对半分(分到最后只有单个对单个的元素);然后两个有序数组合并。
// 归并排序-递归
void Merge(vector<int> A, int low, int mid, int high){
vector<int> B(A);
int i=low,j=mid+1,k=i;
while(i<=mid&&j<=high){
if(B[i]<=B[j]){ A[k]=B[i++];} // 比较B左右两段子数组的元素,较小的值复制到A
else{ A[k]=B[j++];}
k++;
}
while(i<=mid) A[k++]=B[i++]; // 有一边遍历完了,剩下的都直接放到A里。
while(j<=high)A[k++]=B[j++];
}
void MergeSort(vector<int>A, int left, int right){
if(left<right){
int mid = left+(right-left)/2;
MergeSort(A,left,mid);
MergeSort(A,mid+1,right);
Merge(A,left,mid,right);
}
}
非递归法详见LeetCode题解
变治法
预排序
- 检验数的唯一性,预先排序再比较O(nlogn)
- 模式计算:先排序,再选出相邻次数最多的等值即可。
堆排序(树形选择排序)
- 堆:
- 视为完全二叉树的顺序存储结构。每个节点都比其左右子树大(最大堆、大根堆)或小(最小堆、小根堆)。
- 从根节点到任意结点路径上的结点序列都有序。
- 堆的操作:
- 【插入】 T ( n ) = O ( l o g 2 n ) T(n)=O(log{_2}{n}) T(n)=O(log2n) 插到尾部,再交换调节,保持有序
- 【删除】
取出根节点元素,同时删除堆的结点a,让a去替换原来的根节点;
然后找出新根节点的较大孩子,与新的根比较大小,更大的作为根节点;若交换了,再比较a的子树。O(logn) - 【建堆】最坏的情况下需要移动元素次数 = 树中各节点下沉高度和 O(N) ,即:叶结点下沉高度为0
- 堆排序思想:
输出堆顶元素(最值)后,将剩余的n-1个元素序列重新建成堆,则可得到次大(小)值,反复执行,直到只剩1个元素。 - 堆排序:
注:最后一个结点是第 ⌊ n 2 ⌋ \left\lfloor \frac { n }{ 2 } \right\rfloor ⌊2n⌋个结点a的孩子。对a为根的子树进行筛选,使得该子树成为堆。之后,向前依次对以前一半结点为根的子树进行筛选。
- 空间效率:O(1)
- 建立大根堆:时间复杂度O(n) (向下调整时因为大部分结点高度较小),后面有n-1次向下调整的操作,每次调整时间复杂度为O(h)
- 时间复杂度:先构造堆,再删除最大键,降序删除,好坏平均都是:O(nlogn)
- 堆排序总体代码实现:
注:父节点是k,左子结点是2k+1,右子节点是2k+2
// 堆排序-向下调整
void moveDown(vector<int>& array, int first, int last){
int curIndex = first * 2 + 1; // first的左子节点索引
while (curIndex <= last){
// 找first为根节点的左右两节点(若存在)更大的那个,将其脚标赋值给curIndex
if (curIndex < last && array[curIndex] < array[curIndex + 1]){
curIndex++;
}
// 将更大的子节点与根节点比较,更大的置于根节点。即:若根节点值小于子节点值,则交换
if (array[first] < array[curIndex]){
swap(array[first], array[curIndex]);
first = curIndex; // 还需要继续向下看交换后,根节点的孙子树等是否受到影响
curIndex = first * 2 + 1;
}
else{ // 若根节点大,那就不必调整了。
break;
}
}
}
// 从最下面的部分开始,把它和子树调整成堆的形式;
// 然后沿着数组往前(堆往上的一层)调整成更大的堆。
void buildHeap(vector<int>& array){ // 用数组实现堆
int arrsize = (int)array.size();
for(int i = arrsize/2 - 1; i>=0; i--){ // 最后一个非叶节点的节点索引
moveDown(array, i, arrsize-1);
}
}
// 堆排序
void heapSort(vector<int>& array){
// 生成堆
buildHeap(array);
// 堆顶、底索引
int first = 0, last = (int)array.size() - 1;
while (first <= last){
swap(array[first], array[last]); // 堆顶和堆底元素交换,堆顶元素坠到数组尾端
moveDown(array, first, --last); // 整理,把剩余的i-1个元素重新整理成堆
}
}
- 图解
计数排序
基数排序
- 采用多关键字排序思想(基于关键字各位的大小进行排序),不需要比较关键字的大小。
- 分为MSD(最高位优先)和MLD(最低位优先:从个位数开始排序,而后是十位数等等)
- 空间效率:一趟排序的辅助存储空间:r 个队列,以后会重复使用。
- (关键字的值∈[0,r-1],比如数字是0~9)
- 时间效率:n个结点,需要d趟分配和收集,一趟分配需要O(n),一趟收集需要O( r),时间复杂度为:O(d(n+r)),与序列的初始状态无关
- (每个结点的关键字由d元组组成,比如:数字是d位数)
- 稳定的。
- 适合字符串和整数这种有明显结构特征的关键码。
桶排序
- 将待排序集合中处于同一值域的元素放入一个桶中,拆分后形成多个桶,对每个桶中的元素进行排序(可任选排序方法)。