前言
排序是计算机科学中的一个基本操作,因此学好排序的重要性不用多说。
本文适用于十大经典排序的复习回顾,或作学习时的目录大纲。
1、选择排序
工作原理 :
首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
伪代码:
SELECTION-SORT(A)
for j = 1 to A.Length
i = j
key = A(i)
//select min from the remaining unsorted elements
for i to A.Length
if key>A(i)
key = A(i)
k = i
A(k) = A(j)
A(j) = key
C++/C 核心代码:
//template<class T> int
void selection_sort(int A[], int n)
{
for (int i = 0; i < n; i++)
{
int index = i;
for (int j = i + 1; j < n; j++)
{
if (A[index] > A[j])
index = j;
}
if (index != i)
{
//swap
int temp = A[i];
A[i] = A[index];
A[index] = temp;
}
}
}
时间复杂度 : 平均 O(n2)
2、插入排序
工作原理 :
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即 原位操作,不需要用到额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
伪代码 :
INSERTION-SORT(A)
for j = 2 to A.Length
key = A[j]
//Insert A[j] into the sorted sequence A[1..j - 1].
i = j - 1
while i > 0 and A[i] > key
A[i + 1] = A[i]
i = i - 1
A[i + 1] = key
C++/C 核心代码:
void insert_sort(int *A,int n)
{
int i;
int key = 0;
for (int j = 2; j <= n; j++)
{
key = A[j];
i = j - 1;
while (i > 0 && A[i] > key)
{
A[i + 1] = A[i];
i--;
}
A[i + 1] = key;
}
}
时间复杂度 :平均 O(n2)
3、冒泡排序
工作原理 :
重复地走访过要排序的数列,一次比较两个相邻元素,如果他们的顺序(如从大到小)错误就把他们交换过来。
-
第一趟把最大的元素放到最后的位置,第二趟把倒数第二大的元素放到倒数第二个的位置,第三趟把第三大的元素放在倒数第三的位置上…依次类推
伪代码 :
BUBBLE-SORT(A)
for i = 0 to A.Length - 1
for j = 0 to A.Length - i - 1
if A[j] > A[j + 1]
exchange A[j] with A[j + 1]
C++/C 核心代码:
void bubble_sort(int* A, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
//swap
if (A[j] > A[j + 1])
{
int temp = A[j];
A[j] = A[J + 1];
A[j + 1] = temp;
}
}
}
}
时间复杂度 :平均 O(n2)
4、希尔排序
希尔排序是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
工作原理 :
希尔排序基于插入排序的2种性质进行改进
1、在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
2、但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
按照不同步长(间隔) 对元素进行 插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高
关于希尔排序的时间复杂度,与步长息息相关,因此步长的选择是希尔排序的重要部分。
伪代码 :
SHELL-SORT(A)
h = 1;
//The last step(gap) must be 1
Select-step(gap)
while h >= 1
for i = h to A.Length - 1
for j = i downto h step h
if A[j] < A[j - h]
//swap
temp = A[j]
A[j] = A[j - h]
A[j - h] = temp
Change-step(gap)
C++/C 核心代码 :
void shell_sort(int* A, int n)
{
int h = 1;
//Calculate the first step
while (h < n / 3)
h = h * 3 + 1;
while (h >= 1)
{
for (int i = h; i < n; i++)
{
for (int j = i; j >= h; j -= h)
{
if (A[j] < A[j - h])
{
int temp = A[j];
A[j] = A[j - h];
A[j - h] = temp;
}
}
}
h = h / 3;
}
}
时间复杂度 :平均 O(n1.5)
5、归并排序
讲归并排序之前一定要讲什么是分治(Divide and Conquer)
分治 :
将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题地解来建立原问题的解
-
因此分治模式在每层递归时都有三个步骤:分解、解决、合并
工作原理 :
归并排序 算法完全遵循分治模式。直观上其操作如下
分解: 分解待排序的n个元素的序列成各具 n 2 \frac{n}{2} 2n个元素的两个子序列
-
解决: 使用归并排序递归地排序两个子序列
-
合并: 合并两个已排序的子序列以产生已排序的答案
当待排序的序列长度为1时,递归“开始回升”, 在这种情况下不要做任何工作, 因为长度为1的每个序列都已排好序
伪代码:
MERGE-SORT(A, x, y, T)
if y - x > 1
m = (x + y)/2
p = x, q = m, i = x
//let L[x...m - 1] and R[m... y - 1] be new arrays
MERGE-SORT(A, x, m, T)
MERGE-SORT(A, m, y, T)
while p < m or q < y
if q >= y or (p < m and A[q] >=A[p])
T[i++] = A[p++]
else T[i++] = A[q++]
for i = x to y - 1
A[i] = T[i]
C++/C 核心代码 :
void merge_sort(int* A, int x, int y, int* T)
{
if (y - x > 1)
{
int m = (x + y) / 2;
int p = x, q = m, i = x;
merge_sort(A, x, m, T);
merge_sort(A, m, y, T);
while (p < m || q < y)
{
if (q >= y || (p < m && A[q] >= A[p])) T[i++] = A[p++];
else
T[i++] = A[q++];
}
for (int i = x; i < y; i++) A[i] = T[i];
}
}
时间复杂度 :平均 O(nlogn)
6、快速排序
与归并排序一样,快速排序也使用了分治思想
工作原理 :
分解: 数组A被划分为两个(可能为空)子数组,使得其中一个子数组中每一个元素都小于等于设定值,另一个子数组中每一个元素都大于等于设定值
-
解决: 同归递归调用快速排序,对两个子数组进行快速排序
-
合并: 因为子数组都是原址排序(不需要额外一个数组),所以不需要合并操作,数组A已经有序
因为随着设定值选择的不同(通常为首关键字、中关键字和尾关键字),就有不同版本的快速排序,这里选择了中关键字
伪代码:
QUICK-SORT(A, l, r)
if l < r
mid =A[(r + l)/2]
i = l, j = r
while i<=j
while A[i] < mid i++
while A[j] > mid j--
if i <= j
swap(A[i++], A[j--])
QUICK-SORT(A, l, j)
QUICK-SORT(A, i, r)
C++/C 核心代码 :
//template<typename T> int
void quicksort(int* A, int l, int r)
{
if (l < r)
{
int mid = A[(r + l) / 2];
int i = l, j = r;
while (i <= j)
{
while (A[i] < mid) i++;
while (A[j] > mid) j--;
if (i <= j)
swap(A[i++], A[j--]);
}
quicksort(A, l, j);
quicksort(A, i, r);
}
}
时间复杂度 :平均 O(nlogn)
7、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法
因此需要先介绍什么是堆:
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(在堆排序算法中用于升序排列);或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(在堆排序算法中用于降序排列)。
接下来讲解怎么由堆的性质构造堆(heap) :
(二叉)堆 可以被看成一个**近似的完全二叉树**(见《数据结构》)
则树上的每一个结点对应数组中的一个元素,因此可以用一个数组来表示,同时给定一个结点的下标 i ,很容易计算得到它的父结点、左孩子和右孩子的下标
结点 i | 下标 |
---|---|
PARENT(i) | i>>1 (
i
2
\frac{i}{2}
2i) |
LEFT(i) | i<<1 (2i) |
RIGHT(i) | i<<1|1 (2i+1) |
最大堆性质: 除根以外的所有结点 i 都要满足 A[PARENT(i)] >= A[i] (最小堆性质 A[PARENT(i)] <= A[i])
则需要在插入和删除元素的时候进行维护,即调整元素位置
这里就直接贴出了最小堆的核心代码:
template<class T,int MAXN>
class Heap {
public:
int heapsize = 1;
T* heap = new T[MAXN + 1];
inline int left(int index) { return index << 1; }
inline int right(int index) { return index << 1 | 1; }
inline int father(int index) { return index >> 1; }
void push(T key)
{
heap[heapsize] = key;
heapsize++;
int i = heapsize - 1;
while(i!=1)
{
if (heap[i] < heap[father(i)])
{
swap(heap[i], heap[father(i)]);
i = father(i);
}
else
break;
}
}
void pop()
{
heap[1] = heap[heapsize - 1];
heapsize--;
heap[heapsize] = 0;
int i = 1;
while (left(i) < heapsize)
{
int small = left(i);
if (right(i) < heapsize && heap[right(i)] < heap[small])
small = right(i);
if (heap[small] < heap[i])
swap(heap[small], heap[i]), i = small;
else
break;
}
}
T top() { return heap[1]; }
};
若像本文这里直接构造了最小(最大)堆,则直接输入,输出即为已排序好的,其他亦可,思路是一致的
时间复杂度 :平均 O(nlogn)
8、计数排序
计数排序是一种牺牲空间换取时间的排序,适合于最大值和最小值的差值不是很大的排序。
工作原理 :
对每一个输入元素 x ,确定小于 x 的元素个数(即 直接统计每个数出现的次数,然后相加得 小于等于 x 的数出现的次数),利用这一信息,就可以直接把 x 放到它在输出排列中应在的位置
伪代码 :
COUNTING-SORT(A, B, k) //0-k 区间
let C[0...k] be a new array
min = min(A)
for i = 0 to k
C[i] = 0 //memset
for j = 1 to A.Length
C[A[j] - min] += 1
//C[i] now contains the number of elements equal to i + min
for i = 1 to k
C[i] = C[i] + C[i - 1]
//C[i] now contains the number of elements less than or equal to i + min
for j = A.Length downto 1
B[C[A[j] - min]] = A[j]
C[A[j] - min] -= 1
C++/C 核心代码 :
void counting_sort(int* A, int* B, int k, int n)
{
int min = A[0];
int* C = new int[k + 1];
for (int i = 1; i < n; i++)
min = A[i] < min ? A[i] : min;
for (int i = 0; i <= k; i++)
C[i] = 0;
for (int j = 1; j <= n; j++)
C[A[j] - min] = C[A[j] - min] + 1;
for (int i = 1; i <= k; i++)
C[i] = C[i] + C[i - 1];
for (int j = n ; j >= 1; j--)
{
B[C[A[j] - min]] = A[j];
C[A[j] - min] = C[A[j] - min] - 1;
}
delete[] C;
}
时间复杂度 :平均 O(n+k)
9、基数排序
基数排序(radix sort) 又称“桶子法”(bucket sort),顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。
工作原理 :
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零(一般采用10进制)。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后(期中操作员必须保证卡片从容器中被取出时不改变顺序), 数列就变成一个有序序列。
特殊地,我们也可以用基数排序来对具有多关键字域的记录进行排序,比如日期(年,月,日)。
伪代码 :
RADIX-SORT(A, d)
for i = 1 to d
use a stable sort to sort array A on digit i
C++/C 核心代码 :
int MaxBit(int* A,int N)
{
int cnt = 1, r = 10;
for (int i = 1; i <= N; i++)
while (A[i] >= r)
r *= 10, cnt++;
return cnt;
}
void radix_sort(int *a,int n)
{
int bit = MaxBit(a,n);
int radix = 1;
int count[10] = { 0 }; //count
int* temp = new int[n + 1];
for (int i = 1; i <= bit; i++)
{
memset(count, 0, sizeof(count));
for (int i = 1; i <= n; i++)
count[(a[i] / radix) % 10]++;
for (int i = 1; i < 10; i++)
count[i] = count[i - 1] + count[i];
//Ensure that the order of the cards is not changed when they are taken out of the container
for (int i = n; i >= 1; i--)
{
temp[count[(a[i] / radix) % 10]] = a[i];
count[(a[i] / radix) % 10]--;
}
for (int i = 1; i <= n; i++)
a[i] = temp[i];
radix *= 10;
}
delete[] temp;
}
时间复杂度 :平均 O(n*k)
10、桶排序
桶排序(bucket sort) 与计数排序类似,对输入数据作了某种假设,具体来说,计数排序假设输入数据都属于一个小区间内的整数。而桶排序则假设输入数据是由一个随机过程产生,该过程将元素均匀、独立地分布在[ 0, 1)区间上(详见概率论中均匀分布)
工作原理 :
1、根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数(将[ 0, 1) 区间划分为 n 个相同大小的子区间);
-
2、遍历待排序集合,将每一个元素移动到对应的桶中;
-
3、对每一个桶中元素进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来。
伪代码 :
BUCKET-SORT(A)
n =A.Length
Construct linked list B
for i = 1 to n
insert A[i] into list B
for i = 0 to n - 1
sort list B[i] with insertion sort
concatenate the lists B[0],B[1],...,B[n - 1] together in order
下面为方便以 映射规则 f(x) = x/10 - c ,其中 c 为 min/10 即以间隔大小10来区分不同值域
C++/C 核心代码 :
struct KeyNode
{
int key;
struct KeyNode* next;
};
void bucket_sort(int *A, int n)
{
int c = A[0];
int max = A[0];
for (int i = 1; i < n; i++)
{
c = c < A[i] ? c : A[i];
max = max < A[i] ? A[i] : max;
}
c = c / 10;
int bucket_size = max / 10 - c + 1;
KeyNode** bucket_table = new struct KeyNode*[bucket_size];
//Initialization
for (int i = 0; i < bucket_size; i++)
{
bucket_table[i] = new struct KeyNode;
bucket_table[i]->key = 0;
bucket_table[i]->next = NULL;
}
//insert and sort
for (int i = 0; i < n; i++)
{
KeyNode* node = new struct KeyNode;
node->key = A[i];
node->next = NULL;
int index = A[i] / 10 - c;
KeyNode* p = bucket_table[index];
if (p->key == 0)
{
p->next = node;
p->key++;
}
else
{
while (p->next != NULL && p->next->key <= node->key)
p = p->next;
node->next = p->next;
p->next = node;
(bucket_table[index]->key)++;
}
}
//output
KeyNode* k = NULL;
for (int i = 0; i < bucket_size; i++)
{
for (k = bucket_table[i]->next; k != NULL; k = k->next)
{
printf("%d ", k->key);
}
}
}
时间复杂度 :平均 O(n+k)
总结
稳定排序:
假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变 ,则这种排序方法是稳定的。
则选择,快速,希尔,归属于不稳定排序,其他皆稳定
就地(原地)排序:
若是基本上不需要额外辅助的空间,允许少量额外的辅助变量进行的排序,则为原地排序
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | in-place | 不稳定 |
插入排序 | O(n2) | O(n2) | O(n) | O(1) | in-place | 稳定 |
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | in-place | 稳定 |
希尔排序 | O(n1.3) | O(n2) | O(n) | O(1) | in-place | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | out-place | 稳定 |
快速排序 | O(nlogn) | O(n2) | O(nlogn) | O(logn) | in-place | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | in-place | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | out-place | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | out-place | 稳定 |
桶排序 | O(n+k) | O(n2) | O(n) | O(n+k) | out-place | 稳定 |
参考:
- 排序百度百科
- 《算法导论(第3版)》
大法好啊 - 《数据结构与算法》
- 冰狼爱魔 博客园 《十大经典排序》https://www.cnblogs.com/itsharehome/p/11058010.html