此博客整理了一些常用的排序算法,特别是为每种排序算法加上了动图展示。
其中大部分的工作只是搬运,仅限于学习用!
搬运的内容来自wiki和一篇博客,博客链接如下:博客链接
插入排序
动图展示
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 O ( 1 ) O(1) O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法步骤
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 1、从第一个元素开始,该元素可以认为已经被排序
- 2、取出下一个元素,在已经排序的元素序列中从后向前扫描
- 3、如果该元素(已排序)大于新元素,将该元素移到下一位置
- 4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 5、将新元素插入到该位置后
- 6、重复步骤2~5
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找插入排序。
复杂度
- 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 最优时间复杂度 O ( n ) O(n) O(n)
- 平均时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 最坏空间复杂度 总共 O ( n ) O(n) O(n) ,需要辅助空间 O ( 1 ) O(1) O(1)
C++实现
void insertion_sort(int arr[],int len){
for(int i=1;i<len;i++){
int key=arr[i];
int j=i-1;
while((j>=0) && (key<arr[j])){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=key;
}
}
选择排序
动图展示
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,将其放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法步骤
- 1、在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 2、再从剩余未排序元素中继续寻找最小(大)元素,将其放到已排序序列的末尾(也可以通过交换)
- 3、重复步骤2
复杂度
- 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 最优时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 平均时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 最坏空间复杂度 O ( n ) O(n) O(n) total, O ( 1 ) O(1) O(1) auxiliary
C++实现
template<typename T> //整數或浮點數皆可使用,若要使用物件(class)時必須設定大於(>)的運算子功能
void selection_sort(std::vector<T>& arr) {
for (int i = 0; i < arr.size() - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.size(); j++)
if (arr[j] < arr[min])
min = j;
std::swap(arr[i], arr[min]);
}
}
冒泡排序
动图展示
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤
冒泡排序算法的运作如下:
- 1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 3、针对所有的元素重复以上的步骤,除了最后一个。
- 4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
复杂度
- 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 最优时间复杂度 O ( n ) O(n) O(n)
- 平均时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 最坏空间复杂度 总共 O ( n ) O(n) O(n) ,需要辅助空间 O ( 1 ) O(1) O(1)
C++实现
#include <iostream>
using namespace std;
template<typename T> //整数或浮点数皆可使用,若要使用类(class)或结构体(struct)时必须重载大于(>)运算符
void bubble_sort(T arr[], int len) {
int i, j;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
swap(arr[j], arr[j + 1]);
}
int main() {
int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 };
len = (int) sizeof(arrf) / sizeof(*arrf);
bubble_sort(arrf, len);
for (int i = 0; i < len; i++)
cout << arrf[i] << ' '<<endl;
return 0;
}
希尔排序
动图展示
希尔排序(shell sort),也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
算法步骤
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 1、选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 2、按增量序列个数k,对序列进行k 趟排序;
- 3、每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
复杂度
- 最坏时间复杂度 根据步长序列的不同而不同。已知最好的: O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)
- 最优时间复杂度 O ( n ) O(n) O(n)
- 平均时间复杂度 根据步长序列的不同而不同
- 最坏空间复杂度 O ( n ) O(n) O(n)
C++实现
template<typename T>
void shell_sort(T array[], int length) {
int h = 1;
while (h < length / 3) {
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < length; i++) {
for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
std::swap(array[j], array[j - h]);
}
}
h = h / 3;
}
}
归并排序
动图展示
归并排序(Merge sort),是创建在归并操作上的一种有效的排序算法,效率为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
算法步骤
递归法(Top-down)
- 1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 2、设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 4、重复步骤3直到某一指针到达序列尾
- 5、将另一序列剩下的所有元素直接复制到合并序列尾
迭代法(Bottom-up)
原理如下(假设序列共有
n
n
n个元素):
- 1、将序列每相邻两个数字进行归并操作,形成 c e i l ( n / 2 ) ceil(n/2) ceil(n/2)个序列,排序后每个序列包含两/一个元素
- 2、若此时序列数不是1个则将上述序列再次归并,形成 c e i l ( n / 4 ) ceil(n/4) ceil(n/4)个序列,每个序列包含四/三个元素
- 3、重复步骤2,直到所有元素排序完毕,即序列数为1
复杂度
- 最坏时间复杂度 O ( n l o g n ) O (nlog n) O(nlogn)
- 最优时间复杂度 O ( n l o g n ) O (nlog n) O(nlogn)
- 平均时间复杂度 O ( n l o g n ) O (nlog n) O(nlogn)
- 最坏空间复杂度 O ( n ) O (n) O(n)
C++实现
迭代版:
template<typename T> // 整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)的運算子功能
void merge_sort(T arr[], int len) {
T *a = arr;
T *b = new T[len];
for (int seg = 1; seg < len; seg += seg) {
for (int start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
T *temp = a;
a = b;
b = temp;
}
if (a != arr) {
for (int i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
delete[] b;
}
递归版:
void Merge(vector<int> &Array, int front, int mid, int end) {
// preconditions:
// Array[front...mid] is sorted
// Array[mid+1 ... end] is sorted
// Copy Array[front ... mid] to LeftSubArray
// Copy Array[mid+1 ... end] to RightSubArray
vector<int> LeftSubArray(Array.begin() + front, Array.begin() + mid + 1);
vector<int> RightSubArray(Array.begin() + mid + 1, Array.begin() + end + 1);
int idxLeft = 0, idxRight = 0;
LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max());
RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max());
// Pick min of LeftSubArray[idxLeft] and RightSubArray[idxRight], and put into Array[i]
for (int i = front; i <= end; i++) {
if (LeftSubArray[idxLeft] < RightSubArray[idxRight]) {
Array[i] = LeftSubArray[idxLeft];
idxLeft++;
} else {
Array[i] = RightSubArray[idxRight];
idxRight++;
}
}
}
void MergeSort(vector<int> &Array, int front, int end) {
if (front >= end)
return;
int mid = (front + end) / 2;
MergeSort(Array, front, mid);
MergeSort(Array, mid + 1, end);
Merge(Array, front, mid, end);
}
快速排序
动图展示
快速排序(Quick sort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序
n
n
n个项目要
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)次比较。在最坏状况下则需要
O
(
n
2
)
O(n^2)
O(n2)次比较,但这种状况并不常见。事实上,快速排序
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
算法步骤
- 1、从数列中挑出一个元素,称为“基准”(pivot),
- 2、重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分割结束之后,该基准就处于数列的中间位置。这个称为分割(partition)操作。
- 3、递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
复杂度
- 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 最优时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
- 平均时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
- 最坏空间复杂度 根据实现的方式不同而不同
C++实现
template <typename T>
void quick_sort_recursive(T arr[], int start, int end) {
if (start >= end)
return;
T mid = arr[end];
int left = start, right = end - 1;
while (left < right) { //在整个范围内搜寻比枢纽元值小或大的元素,然后将左侧元素与右侧元素交换
while (arr[left] < mid && left < right) //试图在左侧找到一个比枢纽元更大的元素
left++;
while (arr[right] >= mid && left < right) //试图在右侧找到一个比枢纽元更小的元素
right--;
std::swap(arr[left], arr[right]); //交换元素
}
if (arr[left] >= arr[end])
std::swap(arr[left], arr[end]);
else
left++;
quick_sort_recursive(arr, start, left - 1);
quick_sort_recursive(arr, left + 1, end);
}
template <typename T> //整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)、"大於"(>)、"不小於"(>=)的運算子功能
void quick_sort(T arr[], int len) {
quick_sort_recursive(arr, 0, len - 1);
}
堆排序
动图展示
堆排序动图,来自bilibili
堆排序(Heap sort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
复杂度
- 最坏时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
- 最优时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
- 平均时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
- 最坏空间复杂度 O ( n ) O(n) O(n) total, O ( 1 ) O(1) O(1) auxiliary
C++实现
#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;
}
基数排序
动图展示
基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。
它是这样实现的:将所有待比较数值(正整数)统一为同样的数字长度,数字较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
复杂度
最坏时间复杂度
O
(
k
N
)
O(kN)
O(kN)
最坏空间复杂度
O
(
k
+
N
)
O(k+N)
O(k+N)
C++实现
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
/* int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;*/
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}