排序算法总结
所有的排序算法,默认: 小的在前 大的在后
参考博客
- https://blog.youkuaiyun.com/misayaaaaa/article/category/6833062
- https://www.cnblogs.com/onepixel/p/7674659.html
- https://github.com/francistao/LearningNotes/blob/master/Part3/Algorithm/Sort/面试中的 10 大排序算法总结.md
- 非线性时间排序:通过元素比较来决定相对次序. 时间复杂度不能突破 O(NlogN)
- 线性时间排序: 不通过比较元素来决定次序. 可以在线性时间运行. 因此称为线性时间非比较类排序;
算法复杂度:
冒泡排序
-
冒泡排序:
它重复走过一次要排序的数列,一次比较两个元素,若他们顺序错误,就进行交换.越小的元素会慢慢 浮到数列前面.
每次确定一个未排序中的最大一个数!!!
算法步骤:
- 比较相邻两个元素,若第一个比第二个大 就交换顺
- 每一对都交换顺序,经过一轮比较之后,最后一个元素就是最大
- 每一轮可以确定一个最大的数.需要 数组个数次
代码实现:
/*
冒泡排序原理:
1. 比较相邻两个元素,若第一个比第二个大 就交换顺序
2. 每一对都交换顺序,经过一轮比较之后,最后一个元素就是最大了.
3. 每一轮可以确定一个最大的数.需要 数组个数次
*/
void bubbleSort(vector<int> &arr) {
int _len = arr.size();
// 外循环,一共进行多少次比较
for (int times = 0; times < _len; times++) {
for (int i = 0; i < _len - times-1; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr[i], arr[i + 1]);
}
}
}
}
- 改进1,2
// 改进1: 用标志来表明是否进行了数据的交换,若无数据交换,说明已经排好序了.
void bubbleSort1(vector<int> &arr) {
int _len = arr.size();
// 外循环,一共进行多少次比较
for (int times = 0; times < _len; times++) {
bool sorted = true;// 是否已经有序
for (int i = 0; i < _len - times - 1; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr[i], arr[i + 1]);
if(sorted)
sorted = false;
}
}
if (sorted)
break;
}
}
// 改进2:每次增加一个反向冒泡,确定最小值
void bubbleSort2(vector<int> &arr) {
int _len = arr.size();
// 外循环,
int low = 0;
int high = _len - 1;
while (low < high) {
for (int i = low; i != high; i++) {
if (arr[i] > arr[i + 1] && i + 1 <= high) {
swap(arr[i], arr[i + 1]);
}
}
high--;// 排完序以后,high位置确定了.所以--
for (int j = high; j != low; j--) {
if (arr[j] < arr[j - 1] && j - 1 >= low) {
swap(arr[j], arr[j - 1]);
}
}
low++;// 排完序以后,low位置确定了.所以++
}
}
算法性能分析:
- 冒泡排序对 n 个元素需要 O(n^2) 的比较次数,且可以原地排序,无需辅助空间。
- 仅适用于对于含有较少元素的数列进行排序。
- 最差时间复杂度 O(n^2)
- 平均时间复杂度 O(n^2)
- 最优时间复杂度 O(n)
- 最差空间复杂度 O(n)
- 辅助空间 O(1)
选择排序
- 选择排序的工作原理
- 首先在未排序排序序列中选择最小的元素,放到起始位置
- 然后在剩余的选择最小的元素放到已排序的末尾
- 直至所有元素有序
代码实现:
void selectionSort(vector<int> &arr) {
int _len = arr.size();
for (int i = 0; i < _len; i++) {
int smallIdx = i;
for (int j = i + 1; j < _len; j++) {
if (arr[j] < arr[smallIdx]) {
smallIdx = j;
}
}
if (smallIdx != i)
swap(arr[i], arr[smallIdx]);
}
}
算法性能分析:
- 最好情况时间:O(n^2)
- 平均时间复杂度:O(n^2)
- 最坏情况时间:O(n^2)
- In-place sort,不稳定
插入排序
- 插入排序,对于未排序的数据,从已排序的序列中向后扫描,找到相应位置并插入.
- 插入排序在实现上,通常采用in-place排序(即只需用到O(1)}的额外空间的排序) 因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法步骤:
- 从第一个元素开始,该元素可以认为已经被排序
- 把第二个元素到最后一个元素当成是未排序序列
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。 (如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的 后面 )
代码实现:
void insertSort(vector<int> &arr) {
int _len = arr.size();
// arr[0]认为已经是有序的
for (int i = 1; i < _len; i++) { // 从1开始执行!
int key = arr[i]; // 拿到当前要移动的这个值
int j = i - 1;// 前面是以前排序的
while ((j >= 0) && (key < arr[j])) {
arr[j + 1] = arr[j]; // 若值比arr[j]小,arr[j]向后移动一位
j--;
}
arr[j + 1] = key; //找到了对应位置赋值
}
}
算法性能分析:
- 最优时间复杂度:当输入数组就是排好序的时候,复杂度为O(n),而快速排序在这种情况下会产生O(n^2)的复杂度。
- 最差时间复杂度:当输入数组为倒序时,复杂度为O(n^2) ;
- 插入排序比较适合 “少量元素的数组排序”!!!
- 插入排序的复杂度和
逆序对的个数
一样. 当数组倒序时, 逆序对的个数为 n(n-1)/2 ; 此时复杂度为O(n^2) ; - 插入排序是
稳定的
in-place
算法!!!
// 链表的插入排序程序
归并排序
- 归并: 将两个
有序
的序列合并成一个有序序列. - 归并采用的是
分治
的思想: 即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。 - 归并排序包括
从上往下
和从下往上
两种方式
算法步骤
- 归并排序需要申请额外空间. 使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置(也可以是末尾)
- 比较两个指针所指向的元素,选择相对小(大)的元素放入到合并空间,并移动指针到下一位置,重复步骤直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
从下往上的归并排序
- 将待排序的数列分成若干个长度为1的子数列
- 将这些数组两两合并;得到若干个长度为2的序列.
- 再将这些序列两两合并…直至一个数列为止…
// array是排序的输入,copy是输出(有序)
void mergeSortCore(vector<int> &array, vector<int> ©, int start, int end) {
if (start == end) {
copy[start] = array[start];
return;
}
int mid = (end + start) / 2;
mergeSortCore(copy, array, start, mid); // 注意:copy和array的参数顺序,经过排序后,array[start,end]是有序的!
mergeSortCore(copy, array, mid + 1, end);
int i = mid;
int j = end;
int copyIndex = end;
while (i >= start&&j >= mid + 1) {
if (array[i] > array[j]) { // 排序后
copy[copyIndex--] = array[i--];
}
else {
copy[copyIndex--] = array[j--];
}
}
while( i >= start) {
copy[copyIndex--] = array[i--];
}
while (j >= mid + 1) {
copy[copyIndex--] = array[j--];
}
return;
}
// 从上到下归并排序算法!!!
void mergeSort(vector<int> &array) {
int len = array.size();
vector<int> copy(array.begin(), array.end());
mergeSortCore(copy, array, 0, len - 1);
}
算法性能分析
- 分治思想, 稳定 , 需要额外空间,非in-place
- 最坏情况运行时间:O(nlgn)
- 最佳运行时间:O(nlgn)
- 最优时间复杂度 O(n)
- 最差空间复杂度 O(n)
##快速排序
- 在平均状况下,排序 n 个项目要Ο(n log n)次比较
- 在最坏状况下则需要Ο(n^2)次比较,但这种状况并不常见。
- 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
算法步骤:
- 从数列中选择一个作为
基准
. pivot - 重新排序数组, 实现: 所有大于 pivot都在它后面, 小于pivot都在它前面
- 划分的这个步骤称作 分区
partition
- 递归的将两个分区进行排序.
代码实现:
int partition(vector<int> &array, int start, int end) {
// 选择一个pivotIndex
int pivotIndex = rand() % (end - start + 1) + start; // 随机选一个
swap(array[pivotIndex], array[end]); // 将pivot换到末尾
int small = start;
for (int i = start; i < end; i++) {
if (array[i] < array[end]) {
swap(array[small], array[i]);
small++;
}
}
swap(array[small], array[end]);
return small;
}
void quciSortCore(vector<int> &array, int start, int end) {
if (start <= end) {
int pivotIndex = partition(array, start, end);
quciSortCore(array, start, pivotIndex - 1);
quciSortCore(array, pivotIndex + 1, end);
}
}
void quciSort(vector<int> &array) {
int len = array.size();
if (len <= 0)
return;
quciSortCore(array, 0, len - 1);
}
算法优化:
- 三路快排
void quciSort3waysCore(vector<int> &arr, int l, int r) {
if (l >= r)
return;
// v为pivot,初始存储在arr[l]的位置
int v = arr[l];
int lt = l; // 保持 [l+1...lt] <v
int gt = r + 1; // 保持 [gt,r]>v
int i = l + 1; // 保持 [lt+1,i) == v
while (i < gt) {
if (arr[i] < v) {
swap(arr[i], arr[lt + 1]);
lt++;
i++;
}
else if (arr[i] > v) {
swap(arr[i], arr[gt - 1]);
gt--;
}
else {
i++;
}
}
swap(arr[l], arr[lt]); // 为什么是lt呢,因为lt++了
quciSort3waysCore(arr, l, lt - 1);
quciSort3waysCore(arr, gt, r);
}
void quciSort3ways(vector<int> &arr) {
int len = arr.size();
if (len <= 0)
return;
quciSort3waysCore(arr, 0, len - 1);
}
算法性能分析:
-
算法改进:
1 选取随机数作为枢轴。
2 使用左端,右端和中心的中值做为枢轴元。
-
复杂度分析:
1 最坏运行时间:当输入数组已排序时,时间为O(n^2)
当然可以通过随机化来改进(shuffle array 或者 randomized select pivot),使得期望运行时间为O(nlogn)。
2 最佳运行时间:O(nlogn)
当输入数组的所有元素都一样时,不管是快速排序还是随机化快速排序的复杂度都为O(n^2)
可以使用
三路快排
-
特点: 不稳定, In-place
堆排序
-
堆:本身就是一个完全二叉树
当二叉树的每个节点都大于等于它的子节点的时候,称为大顶堆,
当二叉树的每个节点都小于它的子节点的时候,称为小顶堆,上图即为小顶堆。
算法步骤:
- 将数组堆化…
- 输出堆顶元素,再调整堆
- 反复直至堆为空
代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
void max_heapify(int arr[], int start, int end) {
//建立父節點指標和子節點指標
int dad = start;
int son = dad * 2 + 1;
while (son <= end) { //若子節點指標在範圍內才做比較
if (son + 1 <= end && arr[son] < arr[son + 1]) //先比較兩個子節點大小,選擇最大的
son++;
if (arr[dad] > arr[son]) //如果父節點大於子節點代表調整完畢,直接跳出函數
return;
else { //否則交換父子內容再繼續子節點和孫節點比較
swap(arr[dad], arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len) {
//初始化,i從最後一個父節點開始調整
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);// 先堆化,
//先將第一個元素和已经排好的元素前一位做交換,再從新調整(刚调整的元素之前的元素),直到排序完畢
for (int i = len - 1; i > 0; i--) {
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
int main() {
int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
int len = (int) sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
return 0;
}
算法性能分析:
- 平均时间复杂度:O(NlogN)
- 空间复杂度: O(1)
希尔排序
- [待续]
计数排序
- 不是基于比较的排序算法
- 在于将输入的数据值转化为键存储在额外开辟的数组空间中!!!
- 计数排序要求输入的数据必须是
有确定范围的整数
- 例如: 计数排序是用来排序0到100之间的数字的最好的算法 (统计年龄?)
算法步骤:
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现次数,存入数组C的第i项
- 对所有的计数累加
- 反向填充目标数组
代码实现:
void countingSort(vector<int> &arr) {
// 先找出arr中的最大值
int k = INT_MIN;
for (auto x : arr) {
if (x > k) {
k = x;
}
}
k += 1; // 为了让数组空间足够大!
int _len = arr.size();
int *p = new int[_len]; // [0,_len-1]
int *q = new int[k + 1]; // [0,k]
// 初始化arr中元素出现次数都为0
for (int i = 0; i < k; i++) {
q[i] = 0;
}
// 统计arr数组中元素的出现次数,存储到q中.
// 值k的下标也为k
for (int i = 0; i < _len; i++) {
q[arr[i]]++;
}
// 将所有计数次数累加,即统计这个元素,以及它之前的元素共出现了几次
// 确定了这个元素的最后位置+1
for (int i = 1; i < k; i++) {
q[i] = (q[i] + q[i - 1]);
}
// 将元素重新输入,次数最小为1,数组开始为0.所以要减一
for (int i = 0; i < _len ; ++i) {
int index = q[arr[i]] - 1;
p[index] = arr[i];
q[arr[i]]--; // 出现出现索引减1
}
// 将排序结果拷贝回去
for (int j = 0; j < _len; j++)
arr[j] = p[j];
// 释放空间
delete[] q;
delete[] p;
}
算法性能分析:
-
计数排序是一个稳定的排序算法
-
当输入的元素是 n 个 0到 k 之间的整数时,
时间复杂度是O(n+k),空间复杂度也是O(n+k)
-
当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。