本文总结了常用的几种内排序算法的原理、实现以及各个算法性能的比较。虽然网络上有不少的大神对排序算法进行了比较详细的介绍,但是,本人一方面是为了总结一下自己的知识系统,另一方面想以更加通俗易懂的方式让广大的初学者对排序算法的思想能快速的理解和掌握。由于作者水平有限错误之处在所难免,望大家批评指正。
本文介绍了常用的内排序算法包括比较排序算法(插入排序、冒泡排序、选择排序、快速排序、归并排序、堆排序)和基于运算的排序算法(基数排序、桶排序)。分别对这些算法从算法思想、伪代码、复杂度和稳定性、算法的Java实现几个方面进行了简单的介绍。
0. 排序算法的简单比较
名称 | 稳定性1 | 时间复杂度 | 空间复杂度 |
---|---|---|---|
插入排序 | 稳定 | 平均/最坏: O(n2) | O(1) |
冒泡排序 | 稳定 | 平均/最坏: O(n2) | O(1) |
选择排序 | 不稳定 | 平均/最坏: O(n2) | O(1) |
快速排序 | 不稳定 | 平均: O(nlogn) 最坏: O(n2) | O(logn) |
归并排序 | 稳定 | 平均/最坏: O(nlogn) | O(n) |
堆排序 | 不稳定 | 平均/最坏: O(nlogn) | O(1) |
基数排序 | 稳定 | 平均/最坏: O(k∗n) | O(k∗n) |
桶排序 | 稳定 | 平均: O(n+k) 最坏: O(n2) | O(n∗k) |
1. 插入排序
算法思想:插入排序的工作方式像人们排序一手扑克牌一样。开始时,我们的左手为空并且桌子上的牌面朝下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较,如图1.1所示。
图1.1 使用插入排序来排序手中的扑克牌(图片来源《算法导论》第三版)
图1.2和图1.3给出了插入排序的过程图。
图1.2 使用插入排序为一列数字进行排序的过程(图片来源维基百科)
图1.3 使用插入排序为一列数字进行排序的过程(图片来源维基百科)伪代码:
insertSort(ArrayType num)
for i = 1 to num.length
x = num[i]
for j = i - 1 to 0 && num[j] > x
num[j+1] = num[j]
num[j+1] = x
算法复杂度和稳定性:
平均/最差时间复杂度: O(n2)
空间复杂度: O(1)
稳定性:稳定Java实现:
public static void insertSort(int[] num) {
if (2 > num.length) return;
for (int i = 1; i < num.length; i++) {
int tmp = num[i];
int j = i - 1;
for (; j >= 0 && num[j] > tmp; j--) {//调整数组位置,为tmp腾出空位
num[j + 1] = num[j];
}
num[j + 1] = tmp; //将tmp插入到合适的位置
}
}
2. 冒泡排序
- 算法思想:(升序排列)从起始元素开始,对数组中两两相邻的元素进行比较,将值较小的元素放在前面,值较大的元素放在后面,一轮比较完毕,一个最大的数沉底成为数组中的最后一个元素,一些较小的数如同气泡一样上浮一个位置(因此成为冒泡或者起泡排序)。n个数,经过n-1轮比较后完成排序。
图2.1 冒泡排序的过程
图2.2 使用冒泡排序为一列数字进行排序的过程(图片来源维基百科)
图2.3 使用冒泡排序为一列数字进行排序的过程(图片来源维基百科) - 伪代码:
bubbleSort(ArrayType num)
for i = 0 to num.length
for j = 0 to num.length - i - 1
if num[j] > num[j + 1]
num[j] <-> num[j+1]
- 算法复杂度和稳定性:
平均/最坏的时间复杂度: O(n2)
空间复杂度: O(1)
稳定性:稳定 - Java实现:
public static void bubbleSort(int[] num) {
if (num.length < 2) return;
for (int i = 0; i < num.length; i++) {
for (int j = 0; j < num.length - i - 1; j++) {
if (num[j] > num[j + 1]) {
int tmp = num[j];
num[j] = num[j + 1];
num[j + 1] = tmp;
}
}
}
}
3. 选择排序
- 算法思想:(升序排序)首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
图3.1 使用选择排序为一列数字进行排序的过程(图片来源维基百科)
图3.2 选择排序的示例动画。(红色表示当前最小值,黄色表示已排序序列,蓝色表示当前位置。)(图片来源维基百科) - 伪代码:
selectSort(ArrayType num)
for i = 0 until num.length - 1
int min = i
for j = 0 until num.length
if num[min] > num[j]
min = j
if i != min
num[min] <-> num[i]
- 算法复杂度和稳定性:
平均/最坏的时间复杂度: O(n2)
空间复杂度: O(1)
稳定性:不稳定 - Java实现:
public static void selectSort(int[] num) {
if(num.length < 2) return;
for (int i = 0; i < num.length - 1; i++) {
int min = i;
for (int j = i + 1; j < num.length; j++) {
if (num[min] > num[j])
min = j;
}
if (i != min) {
int tmp = num[i];
num[i] = num[min];
num[min] = tmp;
}
}
}
4. 快速排序
- 算法思想: 快速排序是对冒泡排序的一种改进。通过一趟排序将待排序记录分割成独立的两个部分,其中一部分记录的关键字均小于另一部分记录的关键字,然后再对这两个部分继续进行分割,直至整个序列有序。快速排序使用一个枢轴进行划分,把序列划分成两个部分。
图4.1 使用快速排序法对一列数字进行排序的过程(图片来源维基百科)
图4.2 快速排序采用“分而治之、各个击破”的观念,此为原地(In-place)分区版本。(图片来源维基百科) - 伪代码:
//主程序的递归过程
quickSort(ArrayType num, int low, int high)
if low < high
loc = qSort(num, low, high)
quickSort(num, low, loc - 1)
quickSort(num, loc + 1, high)
//一趟快排
qSort(ArrayType num, int low, int high)
num[0] = num[low] //使用num[0]来存放枢轴记录
pivot = num[low].key
while low < high
while low < high && num[high].key >= pivot
high--
num[low] = num[high]
while low < high && num[low].key <= pivot
low++
num[high] = num[low]
num[low] = num[0]
return low
- 算法复杂度和稳定性:
平均时间复杂度: O(nlogn)
最坏的时间复杂度: O(n2)
空间复杂度: O(logn)
稳定性:不稳定 - Java实现:
public static void qucikSort(int[] num, int low, int high){
if(num.length < 2) return;
if(low < high){
int loc = qSort(num, low, high);
quickSort(num, low, loc - 1);
quickSort(num, loc + 1, high);
}
}
private static int qSort(int[] num, int low, int high){
int pivot = num[low];//这里只存放了记录的key值,对于有卫星数据的记录,需要记录完整的数据元素
while(low < high){
while(low < high && num[high] >= pivot) high--;
num[low] = num[high];
while(low < high && num[low] <= pivot) low++;
num[high] = num[low];
}
num[low] = pivot;
return low;
}
5. 归并排序
- 算法思想: 归并的含义是将两个或者两个以上的有序表组合成一个新的有序表。有序表的合并这里就不在累述。对于两两归并的有序表被称为2-路归并,2-路归并的核心操作是将数组中前后相邻的两个有序序列归并成为一个有序序列。
图5.1 使用归并排序法对散点进行排序的过程(图片来源维基百科)
图5.2 使用归并排序法对一列数字进行排序的过程(图片来源维基百科) - 伪代码:
mergeSort(ArrayType num, int low, int high)
if low < high
mid = floor((low + high)/2)
mergeSort(num, low, mid)
mergeSort(num, mid + 1, high)
merge(num, low, mid, high)
//一次归并
merge(ArrayType num, int low, int mid, int high)
n1 = mid - low + 1
n2 = high - mid
create left[n1+1] and right[n2+1]
for i = 0 to n1 - 1
left[i] = num[low + i]
for j = 0 to n2 - 1
right[j] = num[mid + j + 1]
left[n1]= right[n2] = MAX_VALUE //使用两个哨兵
i = j = 0
for k = low to high
num[k] = left[i] < right[j] ? left[i++]:right[j++]
- 算法复杂度和稳定性:
平均/最坏时间复杂度: O(nlogn)
空间复杂度: O(n)
稳定性:稳定 - Java实现:
public static void mergeSort(int[] num, int low, int high){
if(low < high) {
int mid = (low + high) >>> 1;
mergeSort(num, low, mid);
mergeSort(num, mid + 1, high);
merge(num, low, mid, high);
}
}
private static void merge(int[] num, int low, int mid, int high){
int n1 = mid - low + 1;
int n2 = high - mid;
int[] left = new int[n1 + 1];
int[] right = new int[n2 + 1];
for (int i = 0; i < n1; i++) {
left[i] = num[low + i];
}
for (int i = 0; i < n2; i++) {
right[i] = num[mid + i + 1];
}
left[n1] = Integer.MAX_VALUE;
right[n2] = Integer.MAX_VALUE;
int i = 0, j = 0;
for (int k = low; k <= high; k++) {
num[k] = left[i] < right[j] ? left[i++] : right[j++];
}
}
6. 堆排序
算法思想: 堆是一个近似完全二叉树的结构,并同时满足堆的性质,即子结点的键值或索引总是小于(或者大于)它的父节点。为了讨论方便本文使用最大堆结构对数据进行排序。堆排序可以分解成三个部分排序部分(headSort)、建堆部分(buildMaxHeap)和维护堆(maxHeapify)。
有关堆的创建和维护如有需要以后再补充
图6.1 堆排序算法的演示。首先,将元素进行重排,以匹配堆的条件。图中排序过程之前简单的绘出了堆树的结构。(图片来源维基百科)伪代码:
heapSort(ArrayType num)
buildMaxHeap(num) //创建最大堆
for i = num.length - 1 to 2
num[1] <-> num[i] //把数组的第一个元素取出
num[0]--
maxHeapify(num, 1) //维护最大堆结构
buildMaxHeap(ArrayType num)
num[0] = num.length - 1 //使用数组的0号单元存放堆的大小
for i = 1 to (num.length - 1) >> 1
maxHeapify(num, i)
maxHeapify(ArrayType num, int i)
int left = i << 1; //左孩子
int right = i << 1 + 1; //右孩子
int largest = i; //最大值标识
if left <= num[0] && num[left] > num[largest]
largest = left
if right <= num[0] && num[right] > num[largest]
largest = right
if i != largest
num[largest] <-> num[i]
maxHeapify(num, largest)
- 算法复杂度和稳定性:
平均/最坏时间复杂度: O(nlogn)
空间复杂度: O(1)
稳定性:不稳定 - Java实现:
/**
* 堆排序
*/
public static void heapSort(int[] num) {
if(num.length < 2)return;
buildMaxHeap(num);
for (int i = num.length - 1; i > 1; i--) {
int tmp = num[1];
num[1] = num[i];
num[i] = tmp;
num[0]--;
maxHeapify(num, 1);
}
}
/**
* 最大堆的建立
*/
public static void buildMaxHeap(int[] num) {
num[0] = num.length - 1; //使用第一个元素记录堆的大小
for (int i = (num.length - 1) >>> 1; i > 0; i--) {
maxHeapify(num, i);
}
}
/**
* 最大堆的维护
* 这是堆排序最关键的一个步骤
*/
public static void maxHeapify(int[] num, int i) {
int left = i << 1;
int right = i << 1 + 1;
int largest = i;
if (left <= num[0] && num[left] > num[i]) largest = left;
if (right <= num[0] && num[right] > num[largest])largest = right;
if (largest != i) {
int tmp = num[i];
num[i] = num[largest];
num[largest] = tmp;
maxHeapify(num, largest);
}
}
7. 桶排序
- 算法思想: 假设有一组长度为N的待排关键字序列K[1….n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。9
图8.1 桶排序的过程图(图片来源维基百科) - 伪代码:
bucketSort(ArrayType num, int n)
buckets ← new array of n empty lists
for i = 0 to num.length - 1
insert num[i] into buckets[msbits(num[i], k)]
for i = 0 to n - 1
next-sort(buckets[i])
return the concatenation of buckets[0], ..., buckets[n-1]
算法复杂度和稳定性:
平均时间复杂度: O(n)
最坏时间复杂度: O(n2)
空间复杂度: O(n∗k)
稳定性:稳定Java实现:
public static void bucketSort(double[] num) {
int n = num.length;
List bucketList[] = new ArrayList[n];
for (int i = 0; i < n; i++) {
int temp = (int) Math.floor(n * num[i]);
if (null == bucketList[temp])
bucketList[temp] = new ArrayList<Object>();
bucketList[temp].add(num[i]);
}
for (int i = 0; i < bucketList.length; i++) {
if (bucketList[i] != null)
insert(bucketList[i]);
}
int index = 0;
for (int i = 0; i < n; i++) {
if (null != bucketList[i]) {
Iterator<?> it = bucketList[i].iterator();
while (it.hasNext()) {
num[index++] = (Double) it.next();
}
}
}
}
/**
* 用插入排序对每个桶进行排序 从小到大排序
*/
private static void insert(List list) {
if (list.size() > 1) {
for (int i = 1; i < list.size(); i++) {
double temp = (Double) list.get(i);
int j = i - 1;
for (; j >= 0 && ((Double) list.get(j) > (Double) list.get(j + 1)); j--)
list.set(j + 1, list.get(j)); // 后移
list.set(j + 1, temp);
}
}
}
8. 基数排序
- 算法思想:基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序引用http://www.cnblogs.com/jingmoxukong/p/4311237.html - 伪代码:
radixSort(ArrayType A, int d) //d为A中元素最多的位数
for i=1 to d
do use a stable sort to sort array A on digit i
- 算法复杂度和稳定性:
平均/最坏时间复杂度: O(n∗k)
空间复杂度: O(n∗k)
稳定性:稳定 - Java实现:
// 获取x这个数的d位数上的数字
// 比如获取123的1位数,结果返回3
public int getDigit(int x, int d) {
int a[] = { 1, 1, 10, 100}; // 本实例中的最大数是百位数,所以只要到100就可以了
return ((x / a[d]) % 10);
}
public void radixSort(int[] list, int begin, int end, int digit) {
final int radix = 10; // 基数
int i = 0, j = 0;
int[] count = new int[radix]; // 存放各个桶的数据统计个数
int[] bucket = new int[end - begin + 1];
// 按照从低位到高位的顺序执行排序过程
for (int d = 1; d <= digit; d++) {
// 置空各个桶的数据统计
for (i = 0; i < radix; i++) {
count[i] = 0;
}
// 统计各个桶将要装入的数据个数
for (i = begin; i <= end; i++) {
j = getDigit(list[i], d);
count[j]++;
}
// count[i]表示第i个桶的右边界索引
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
// 将数据依次装入桶中
// 这里要从右向左扫描,保证排序稳定性
for (i = end; i >= begin; i--) {
j = getDigit(list[i], d); // 求出关键码的第k位的数字, 例如:576的第3位是5
bucket[count[j] - 1] = list[i]; // 放入对应的桶中,count[j]-1是第j个桶的右边界索引
count[j]--; // 对应桶的装入数据索引减一
}
// 将已分配好的桶中数据再倒出来,此时已是对应当前位数有序的表
for (i = begin, j = 0; i <= end; i++, j++) {
list[i] = bucket[j];
}
}
}
public int[] sort(int[] list) {
radixSort(list, 0, list.length - 1, 3);
return list;
}
参考文献
[1]: 科曼. 算法导论[M]. 机械工业出版社, 2013.
- 稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。 ↩