算法概述

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:

img

各算法对比:

img

冒泡排序

算法思想

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢”浮”到数列的顶端。

算法步骤

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,数组末尾会形成有序区
  3. 针对无序区域元素重复以上的步骤,最多重复数组长度-1次

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 原始实现
*
* @param array
*/
public static void sort1(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}

优化版本

提前退出

基本思想是当某一次比较没有产生交换时,表明数组已经是有序了,因此可以设置一个变量进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 使用变量提前结束
*
* @param array
*/
public static void sort2(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean f = true;
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
f = false;
}
}
if (f) {
break;
}
}
}

记录边界

原始冒泡排序默认无序序列的边界为array.length-i-1,而实际情况不一定,我们可以通过变量记录每次交换后的无序序列的边界,加快排序速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 记录无序序列的边界
*
* @param array
*/
public static void sort3(int[] array) {

int sortBorder = array.length - 1;

for (int i = 0; i < array.length - 1; i++) {
boolean f = true;
int lastChange = sortBorder;
for (int j = 0; j < sortBorder; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
f = false;
lastChange = j;
}
}
sortBorder = lastChange;
if (f) {
break;
}
}
}

双向冒泡

观察以下示例:

img

我们发现2,3,4,5,6,7,8其实是相对又续了的,但因为1的存在却还进行了七轮冒泡,因此我们可以双向冒泡,奇数轮从左到右,偶数轮从右到左冒泡,这也叫鸡尾酒排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 鸡尾酒排序,基于冒泡排序的双向版本
*
* @param array
*/
public static void sort4(int[] array) {

for (int i = 0; i < array.length / 2; i++) {
boolean f = true;
for (int j = i; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
f = false;
}
}
if (f) {
break;
}
f = true;
for (int j = array.length - i - 1; j > i; j--) {
if (array[j - 1] > array[j]) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
f = false;
}
}
if (f) {
break;
}
}
}

选择排序

算法思想

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。

算法步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void sort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
int index = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[index]) {
index = j;
}
}
if (index != i) {
int temp = array[i];
array[i] = array[index];
array[index] = temp;
}
}
}

插入排序

算法思想

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法步骤

  1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
  3. 直到整个序列有序

代码实现

1
2
3
4
5
6
7
8
9
10
11
public static void sort(int[] array) {
for (int i = 1; i < array.length; i++) {
int insertValue = array[i];
int j = i - 1;
while (j >= 0 && insertValue < array[j]) {
array[j + 1] = array[j];
j--;
}
array[j + 1] = insertValue;
}
}

希尔排序

算法思想

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录”基本有序”时,再对全体记录进行依次直接插入排序。

算法步骤

  1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列个数 k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void sort(int[] array) {
int d = array.length;
while (d > 1) {
d /= 2;
for (int i = 0; i < d; i++) {
for (int j = i + d; j < array.length; j += d) {
int insertValue = array[j];
int k = j - d;
while (k >= 0 && array[k] > insertValue) {
array[k + d] = array[k];
k -= d;
}
array[k + d] = insertValue;
}
}
}

}

归并排序

算法思想

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

算法步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static void mergeSort(int[] array, int start, int end) {
if (start >= end) {
return;
}
int mid = (start + end) / 2;
mergeSort(array, start, mid);
mergeSort(array, mid + 1, end);
merge(array, start, mid, end);
}

public static void merge(int[] array, int start, int mid, int end) {
int[] result = new int[end - start + 1];
int index = 0;
int p1 = start;
int p2 = mid + 1;
while (p1 <= mid && p2 <= end) {
if (array[p1] < array[p2]) {
result[index++] = array[p1++];
} else {
result[index++] = array[p2++];
}
}
while (p1 <= mid) {
result[index++] = array[p1++];
}
while (p2 <= end) {
result[index++] = array[p2++];
}
for (int i = 0; i < result.length; i++) {
array[start + i] = result[i];
}
}

快速排序

算法思想

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n²) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

算法步骤

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

代码实现

双边循环法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 快速排序,双边循环法
*
* @param array
* @param startIndex
* @param endIndex
*/
public static void sort1(int[] array, int startIndex, int endIndex) {
if (startIndex >= endIndex) {
return;
}
int left = startIndex;
int right = endIndex;
while (left < right) {
while (array[right] >= array[startIndex] && left < right) {
right--;
}
while (array[left] <= array[startIndex] && left < right) {
left++;
}
int temp = array[left];
array[left] = array[right];
array[right] = temp;
}
//交换标记位置
int temp = array[startIndex];
array[startIndex] = array[left];
array[left] = temp;
sort1(array, startIndex, left - 1);
sort1(array, left + 1, endIndex);
}

单边循环法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 快速排序,单边循环法
*
* @param array
* @param startIndex
* @param endIndex
*/
public static void sort2(int[] array, int startIndex, int endIndex) {
if (startIndex >= endIndex) {
return;
}
int mark = startIndex;
for (int i = startIndex + 1; i <= endIndex; i++) {
if (array[i] < array[startIndex]) {
mark++;
int temp = array[i];
array[i] = array[mark];
array[mark] = temp;
}
}
int temp = array[startIndex];
array[startIndex] = array[mark];
array[mark] = temp;
sort1(array, startIndex, mark - 1);
sort1(array, mark + 1, endIndex);
}

非递归版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 非递归快速排序
*
* @param array
* @param startIndex
* @param endIndex
*/
public static void sort3(int[] array, int startIndex, int endIndex) {
Stack<HashMap<String, Integer>> stack = new Stack<>();
HashMap<String, Integer> map = new HashMap<>(2);
map.put("startIndex", startIndex);
map.put("endIndex", endIndex);
stack.push(map);
while (!stack.isEmpty()) {
HashMap<String, Integer> pop = stack.pop();
Integer s = pop.get("startIndex");
Integer e = pop.get("endIndex");
if (s >= e) {
continue;
}
int mark = s;
for (int i = s + 1; i <= e; i++) {
if (array[i] < array[s]) {
mark++;
int temp = array[i];
array[i] = array[mark];
array[mark] = temp;
}
}
int temp = array[s];
array[s] = array[mark];
array[mark] = temp;
HashMap<String, Integer> m1 = new HashMap<>(2);
HashMap<String, Integer> m2 = new HashMap<>(2);
m1.put("startIndex", s);
m1.put("endIndex", mark - 1);
m2.put("startIndex", mark + 1);
m2.put("endIndex", e);
stack.push(m1);
stack.push(m2);
}

}

堆排序

算法思想

堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)。

算法步骤

  1. 创建一个堆 H[0……n-1];
  2. 把堆首(最大值)和堆尾互换;
  3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  4. 重复步骤 2,直到堆的尺寸为 1。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void downAdjust(int[] array, int parentIndex, int length) {
int childIndex = parentIndex * 2 + 1;
int temp = array[parentIndex];
while (childIndex < length) {
if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
childIndex++;
}
if (temp <= array[childIndex]) {
break;
}
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = childIndex * 2 + 1;
}
array[parentIndex] = temp;
}

public static void heapSort(int[] array) {
//构建小顶堆
for (int i = (array.length - 2) / 2; i >= 0; i--) {
downAdjust(array, i, array.length);
}
for (int i = array.length - 1; i > 0; i--) {
int temp = array[0];
array[0] = array[i];
array[i] = temp;
downAdjust(array, 0, i);
}

}

计数排序

算法思想

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

算法步骤

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

代码实现

原始版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 原始计数排序
*
* @param array
* @return
*/
public static int[] sort1(int[] array) {
//判空
if (array == null || array.length == 0) {
return array;
}
//得到数列最大值
int max = array[0];
for (int k : array) {
if (max < k) {
max = k;
}
}
//统计出现次数
int[] countSort = new int[max + 1];
for (int k : array) {
countSort[k]++;
}
int index = 0;
int[] result = new int[array.length];
//构造结果数组
for (int i = 0; i < countSort.length; i++) {
for (int j = 0; j < countSort[i]; j++) {
result[index++] = i;
}
}
return result;
}

稳定版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 优化版本计数排序(数组长度,稳定)
*
* @param array
* @return
*/
public static int[] sort2(int[] array) {
//判空
if (array == null || array.length == 0) {
return array;
}
//找出最大最小值
int max = array[0];
int min = array[0];
for (int v : array) {
if (max < v) {
max = v;
}
if (min > v) {
min = v;
}
}
//创建统计数组
int[] countArray = new int[max - min + 1];
for (int v : array) {
countArray[v - min]++;
}
//统计数组变形
for (int i = 1; i < countArray.length; i++) {
countArray[i] += countArray[i - 1];
}
int[] result = new int[array.length];
//倒序遍历原始数组
for (int i = array.length - 1; i >= 0; i--) {
result[countArray[array[i] - min] - 1] = array[i];
countArray[array[i] - min]--;
}
return result;
}

桶排序

算法思想

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

算法步骤

  1. 划分桶,将元素加入桶中
  2. 将每个桶内的元素排序
  3. 按顺序取出元素

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public static double[] sort(double[] array) {
//判空
if (array == null || array.length == 0) {
return array;
}
//找出最大最小值
double max = array[0];
double min = array[0];
for (double value : array) {
if (max < value) {
max = value;
}
if (min > value) {
min = value;
}
}
//初始化桶
double d = max - min;
int bucketNum = array.length;
ArrayList<LinkedList<Double>> bucketList = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketList.add(new LinkedList<>());
}

//将元素加入桶
for (double v : array) {
int num = (int) ((v - min) * (bucketNum - 1) / d);
bucketList.get(num).add(v);
}

//对每个桶内元素进行排序
for (LinkedList<Double> doubleLinkedList : bucketList) {
Collections.sort(doubleLinkedList);
}

//输出结果
int index = 0;
double[] result = new double[array.length];
for (LinkedList<Double> doubles : bucketList) {
for (Double aDouble : doubles) {
result[index++] = aDouble;
}
}
return result;
}

基数排序

算法思想

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

算法步骤

  1. 根据不同位进行计数排序
  2. 汇总排序元素

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @Title
* @Description
* @Author WolfMan
* @Date 2021/12/5 21:11
* @Email 2370032534@qq.com
*/
public class Main {

public static final int ASCII_RANGE = 128;

public static void main(String[] args) {
String[] array = {"qd", "abc", "qwe", "hhh", "a", "cws", "ope"};
System.out.println(Arrays.toString(radixSort(array, 3)));
}

public static String[] radixSort(String[] array, int maxLength) {
String[] sortedArray = new String[array.length];
for (int k = maxLength - 1; k >= 0; k--) {
//计数排序
int[] count = new int[ASCII_RANGE];
for (int i = 0; i < array.length; i++) {
count[getCharIndex(array[i], k)]++;
}
//计数排序变形
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
for (int i = array.length - 1; i >= 0; i--) {
int index = getCharIndex(array[i], k);
sortedArray[count[index] - 1] = array[i];
count[index]--;
}
array = sortedArray.clone();
}
return array;
}

public static int getCharIndex(String str, int k) {
if (str.length() < (k + 1)) {
return 0;
}
return str.charAt(k);
}
}

参考文章: