排序
内部排序:待排序的元素总数相对于内存而言较小,整个排序过程可以在内存中进行。
外部排序:待排序的元素总数较多,不能全部放入内存,排序过程中需要访问外存。
内部排序算法有很多,各有其优缺点和适合的场合。
排序的稳定性:如果在排序的序列中,存在前后相同的两个元素的话,排序前和排序后他们的相对位置不发生变化。
稳定性的定义
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,$r_i$=$r_j$,且$r_i$在$r_j$之前,而在排序后的序列中,$r_i$仍在$r_j$之前,则称这种排序算法是稳定的;否则称为不稳定的。
对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
稳定性的意义
- 如果只是简单的进行数字的排序,那么稳定性将毫无意义。
- 如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义。
- 如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
- 除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。
稳定排序
基数排序、冒泡排序、直接插入排序、两两插入排序、归并排序。不稳定排序
堆排序、快速排序、希尔排序、简单选择排序。直接插入排序
基本思想
将数组中第1个元素作为有序序列,然后将剩下的n-1数组元素,按照顺序插入第1个元素的序列中。
插入第1个元素后,第1个元素的序列保持有序。
将待插入有序序列的元素存入临时变量temp中,在有序序列从后往前比较大小,查找插入的位置。
当temp小于有序序列的元素时,该有序序列元素往后移1位,temp元素继续比较。
直到有序序列中有元素比temp小、或者到有序序列第1个元素时结束,这时将temp插入有序序列元素的位置。
插入排序会优于选择排序,理由是它在排序过程中能够利用前部分数组元素已经排好序的一个优势,有效地减少一些比较的次数,当然这种优势得看数组的初始顺序如何,最坏的情况下(给定的数组恰好为倒序)插入排序需要比较和移动的次数将会等于1 + 2 + 3… + n = n * (n + 1) / 2
,这种极端情况下,插入排序的效率甚至比选择排序更差。时间复杂度
最好情况比较次数是O(n)
(有序),最坏情况比较次数O(n*n)
无序。空间复杂度
O(1)
每次都交换一个元素稳定性
插入排序是稳定排序,如果碰到相等的元素,就把新元素插入相等元素的后面,即他们原来的顺序没有变化。(插入排序在全部元素插入完毕之前,都不能确定最终一个元素的位置)temp < A[j - 1]
,如果改成temp <= A[j - 1]
,则是不稳定排序。Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package insert;
public class InsertSort {
void sort(int[] A) {
for (int i = 1; i < A.length; i++) {
int temp = A[i];//待插入的元素存入临时变量
int j = i;
while (j > 0 && temp < A[j - 1]) {//临时变量与排好序的数组元素进行比较(从后往前进行查找),如果临时变量比数组元素值要小,排好序的数组元素往后移1位
A[j] = A[j - 1];//A[j-1]位置往后移
j--;
}
A[j] = temp;//临时元素temp插入A[j]位置,temp>A[j]
}
}
public static void main(String[] args) {
InsertSort insertSort=new InsertSort();
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 101, 56, 17, 18, 23, 34, 15, 35,
25, 53, 51 };
insertSort.sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+",");
}
}
}
C++
高亮数字标记的数字为插入的数字,被划掉的数字是未参与此次排序的元素,高亮数字标记的数字与被划掉数字之间的元素为逐个向后移动的元素,比如第二趟参与排序的元素为[11, 31, 12]
,需要插入的元素为12,但是12当前并没有处于正确的位置,于是我们需要依次与前面的元素31、11
做比较,一边比较一边移动比较过的元素,直到找到第一个比12小的元素11时停止比较,此时31对应的索引1则是12需要插入的位置。
初始: [11, 31, 12, 5, 34, 30, 26, 38, 36, 18]
第一趟: [11, 31
, 12, 5, 34, 30, 26, 38, 36, 18] (无移动的元素)
第二趟: [11, 12
, 31, 5, 34, 30, 26, 38, 36, 18] (31 向后移动)
第三趟: [5
, 11, 12, 31, 34, 30, 26, 38, 36, 18] (11, 12, 31 皆向后移动)
第四趟: [5, 11, 12, 31, 34
, 30, 26, 38, 36, 18] (无移动的元素)
第五趟: [5, 11, 12, 30
, 31, 34, 26, 38, 36, 18] (31, 34 向后移动)
第六趟: [5, 11, 12, 26
, 30, 31, 34, 38, 36, 18] (30, 31, 34 向后移动)
第七趟: [5, 11, 12, 26, 30, 31, 34, 38
, 36, 18] (无移动的元素)
第八趟: [5, 11, 12, 26, 30, 31, 34, 36
, 38, 18] (38 向后移动)
第九趟: [5, 11, 12, 18
, 26, 30, 31, 34, 36, 38] (26, 30, 31, 34, 36, 38 向后移动)
1 | /** |
希尔排序
类型
插入排序。
基本思想
按下标的一定增量分组(把所有序号相隔d的数组元素放一组,d=n/2,n为要排序数的个数),每组中记录的下标相差d,对每组使用直接插入排序算法排序。
随着增量逐渐减少,每组包含的关键词越来越多,然后再用一个较小的增量(d/2)对它进行分组,当增量减至1时,整个文件恰被分成一组,进行直接插入排序后,排序完成。
该方法实质上是一种分组插入方法:比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。
本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。
原因是,当n值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。
当n值减小时每一趟需要和动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。
希尔排序的诞生是由于插入排序在处理大规模数组的时候会遇到需要移动太多元素的问题。希尔排序的思想是将一个大的数组“分而治之”,划分为若干个小的数组。
比如数组[1, 2, 3, 4, 5, 6, 7, 8]
,如果以gap = 2
来划分,可以分为[1, 3, 5, 7]
和[2, 4, 6, 8]
两个数组(对应的,如gap = 3
,则划分的数组为:[1, 4, 7]
、[2, 5, 8]
、[3, 6]
)然后分别对划分出来的数组进行插入排序,待各个子数组排序完毕之后,再减小gap值重复进行之前的步骤,直至gap = 1
,即对整个数组进行插入排序,此时的数组已经基本上快排好序了,所以需要移动的元素会很小很小,解决了插入排序在处理大规模数组时较多移动次数的问题。
希尔排序是插入排序的改进版,在数据量大的时候对效率的提升帮助很大,数据量小的时候建议直接使用插入排序就好了。
时间复杂度
希尔排序的时间复杂度与增量序列的选取有关。例如希尔增量时间复杂度为O($n^2$),希尔排序时间复杂度的下界是$nlog2n$。希尔排序没有快速排序算法快O(n(logn))
,因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比`O(nn)`复杂度的算法快得多。
空间复杂度
O(1)
稳定性
由于多次插入排序,知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定排序。
规模大小
中等规模比较适合,数据量大排序不是最好选择。
基于插入排序的以下两点性质而提出改进方法的:插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
java
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
36package hill;
public class HillSort2 {
public static void main(String[] args) {
shellSort();
}
public static void shellSort() {
int a[] = { 1, 54, 6, 3, 78, 34, 12, 45, 56, 100 };
double d1 = a.length;
int temp = 0;
while (true) {
//增量
d1 = Math.ceil(d1 / 2);
int d = (int) d1;
//对每个分组进行插入排序
for (int x = 0; x < d; x++) {
//第1个位置的增量i
for (int i = x + d; i < a.length; i += d) {
//每个小分组进行插入排序
int j = i - d;
temp = a[i];
for (; j >= 0 && temp < a[j]; j -= d) {
a[j + d] = a[j];
}
a[j + d] = temp;
}
}
if (d == 1)
break;
}
for (int i = 0; i < a.length; i++)
System.out.print(a[i]+",");
}
}
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
45package hill;
import java.util.Arrays;
public class HillSort {
public static int[] a = { 29, 1, 59, 12, 4, 77, 40, 20, 15, 10, 44, 8, 81, 0, 8, 13, 16 };
public static void main(String[] args) {
// 设置循环 - 步长 - 间隔
for (int m = a.length / 2; m > 0; m = m / 2) {
// 根据步长确定需要排序的数组下标索引
//根据步长进行分组的
for (int n = 0; n < a.length; n = n + m) {
// 对特定数组索引构成的数组进行插入排序
shellSort(a, n, m);
}
}
System.out.println(Arrays.toString(a));
}
/**
* 简单的插入排序算法
*
* @param a
* 需要进行插入排序的数组
* @param startIndex
* 插入排序的起始索引
* @param space
* 插入排序的步长
*/
public static void shellSort(int[] a, int startIndex, int space) {
// 循环右边的无序队列 - 从左到右
for (int i = startIndex + space; i < a.length; i += space) {
// 循环插入到左边的有序队列中,并且使队列有序 - 从左到右
for (int j = i - space; j >= startIndex; j -= space) {
if (a[j + space] < a[j]) {
// 移动有序交换位置
int temp = a[j + space];
a[j + space] = a[j];
a[j] = temp;
}
}
}
}
}
C++
1 | /** |
简单选择排序
基本思想
在数组A[n]
寻找最小值元素,最小元素与数组第一个元素进行比较,然后交换。
下一趟排序在A[1]~A[n-1]
中进行,第i趟排序在待排序子序列(A[i-1]~A[n-1]
)中寻找最小值元素,这最小值元素与第一个元素A[i-1]
交换。
时间复杂度
O(N*N)
最好情况比较次数是0(默认就是有序),最坏情况是无序。
比较次数。对于n个元素的序列,找出最小元素需要比较n-1
次。第1回合后,序列只剩下n-1
个元素,下1次找最小元素还需要n-2
次比较。最后直到2个元素需要比较1次。所以最后比较次数总共为(n-1)+(n-2)+...+1=n(n-1)/2
。
最多交换次数为n-1
。
空间复杂度
O(n)
稳定性
进过一趟排序之后,就可以确定元素的位置,选择排序是不稳定排序。比如[4,8,3,4,2,9,7]
,第1个位置选择最小的元素2,与第一个位置的4交换,这破环了原来两个4之间的顺序。
Java
例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
29public class SelectSort {
void sort(int[] A) {
int small;// 数组中最小值
for (int i = 0; i < A.length; i++) {
small = i; //假设,第一个元素最小
for (int j = i + 1; j < A.length; j++) { //与A[1]~A[n]的
if (A[j] < A[small]) {
small = j;// j是最小值,赋值给small
}
}
//swap(A[i], A[small]);// 最小值元素与待排序第一个元素交换位置
int temp = A[i];
A[i] = A[small];
A[small] = temp;
}
}
public static void main(String[] args) {
SelectSort selectSort = new SelectSort();
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 101, 56, 17, 18, 23, 34, 15,
35, 25, 53, 51 };
selectSort.sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + ",");
}
}
}
例2
高亮数字是交换数字。
初始: [38, 17, 16, 16, 7, 31, 39, 32, 2, 11]
i = 0: [2
, 17, 16, 16, 7, 31, 39, 32, 38
, 11] ([38]<->8th [2])
i = 1: [2, 7
, 16, 16, 17
, 31, 39, 32, 38, 11] ([38]<->4th [17])
i = 2: [2, 7, 11
, 16, 17, 31, 39, 32, 38, 16
] ([11]<->9th [16])
i = 3: [2, 7, 11, 16, 17, 31, 39, 32, 38, 16] (无需交换)
i = 4: [2, 7, 11, 16, 16
, 31, 39, 32, 38, 17
] ([17]<->9th [16])
i = 5: [2, 7, 11, 16, 16, 17
, 39, 32, 38, 31
] ([31]<->9th [17])
i = 6: [2, 7, 11, 16, 16, 17, 31
, 32, 38, 39
] ([39]<->9th [31])
i = 7: [2, 7, 11, 16, 16, 17, 31, 32, 38, 39] (无需交换)
i = 8: [2, 7, 11, 16, 16, 17, 31, 32, 38, 39] (无需交换)
i = 9: [2, 7, 11, 16, 16, 17, 31, 32, 38, 39] (无需交换) ->->->->->->
由例子可以看出,选择排序随着排序的进行(i逐渐增大),比较的次数会越来越少,但是不论数组初始是否有序,选择排序都会从i至数组末尾进行一次选择比较,所以给定长度的数组,选择排序的比较次数是固定的:1 + 2 + 3 + …. + n = n * (n + 1) / 2
,而交换的次数则跟初始数组的顺序有关,如果初始数组顺序为随机,则在最坏情况下,数组元素将会交换n次,最好的情况下则可能0次(数组本身即为有序)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* Selection Sorting
*/
SELECTION(new Sortable() {
public <T extends Comparable<T>> void sort(T[] array, boolean ascend) {
int len = array.length;
for (int i = 0; i < len; i++) {
int selected = i;
for (int j = i + 1; j < len; j++) {
int compare = array[j].compareTo(array[selected]);
if (compare != 0 && compare < 0 == ascend) {
selected = j;
}
}
exchange(array, i, selected);
}
}
})
例3
1 | public class selectSort { |
堆排序
基本思想
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
堆的定义如下:具有n个元素的序列$h_1$,$h_2$,…,$h_n$,当且仅当满足($h_i$>=$h_{2i}$,$h_i$>=2i+1)或($h_i$<=$h_{2i}$,hi<=2i+1)(i=1,2,…,n/2)时称之为堆。
在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面n-1
个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
堆排序主要2部分,构建堆和排序。
构建堆
堆是包含N个节点的完全二叉树,每个节点的关键字大于等于双亲节点的关键字。
定义一个关键字temp=A[r]
,temp大于左右子数较小者,将temp与较小节点交换。
排序
将初始化队列构成最大堆,第一趟将堆顶节点A[0]
与A[n-1]
节点进行交换;交换后,使得前n-1
个节点还是堆。第i趟时,将顶节点A[0]
与A[n-i]
节点进行交换,交换后,使得前n-i
个节点还是堆。
简单概括堆排序就是顶节点与末尾节点进行交换,交换之后,重新构建堆。
时间复杂度
建堆的时间复杂度是O(n)
(调用一次);调整堆的时间复杂度是$lgn$,调用n-1
次,所以堆排序的时间复杂度是O($nlgn$)
空间复杂度
O(1)
稳定性
添加新节点不会破坏相同元素的顺序,但删除根节点获取会破坏,因此堆排序是不稳定排序。
Java
例1
初始序列:46,79,56,38,40,84
建堆
交换,从堆中踢出最大数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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66public class HeapSort1 {
public static void main(String[] args) {
HeapSort1 heapSort=new HeapSort1();
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 101, 56, 17, 18, 23, 34, 15, 35,
25, 53, 51 };
int n=a.length;
//建最大的堆
for (int i = (n-2)/2; i >-1; i--) {
heapSort.creatHeap(a, i, n-1);
}
for (int i = n-1; i >0; i--) {
//堆顶元素和堆底元素进行交换
int tmp=a[i];
a[i]=a[0];
a[0]=tmp;
//创建堆
heapSort.creatHeap(a, 0, i-1);
}
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+",");
}
}
/**
*
* @param data
* @param i
* @param j 堆底下标,最后一个数字下标
*/
private void swap(int[] data, int i, int j) {
int tmp=data[i];
data[i]=data[j];
data[j]=tmp;
}
// 创建最大堆
void creatHeap(int[] A, int r, int j) {
// 左节点
int child = 2 * r + 1;
// 做比较的节点
int temp = A[r];
while (child <= j) {
// 找到左右子数中较大的节点
if ((child < j) && (A[child] < A[child + 1])) {
child++;
}
// temp节点>右子节点(大于较大的那个节点)
//退出循环
if (temp >= A[child]) {
break;
}
//子节点值赋值给父节点
A[(child - 1) / 2] = A[child];
//child的子节点,为了下一次循环
child = child * 2 + 1;
}
//与较大的元素的父节点进行交换
A[(child - 1) / 2] = temp;
}
}
例2
1 | package heap; |
冒泡排序
基本思想
冒泡排序通过交换2个元素实现,在数组内,2个相邻元素进行比较。若后者比较小,进行交换,比较n-1
次。第一趟排序结束,最大元素被交换到数组末尾,下一趟排序在A[0]~A[n-2]
中比较。
冒泡排序跟选择排序比较相像,比较次数一样,都为n*(n+1)/2
,但是冒泡排序在挑选最小值的过程中会进行额外的交换(冒泡排序在排序中只要发现相邻元素的顺序不对就会进行交换,与之对应的是选择排序,只会在内层循环比较结束之后根据情况决定是否进行交换),所以在我看来,选择排序属于冒泡排序的改进版。
对于n个元素,相邻元素均要比较,共有n-1
次。经过一回合冒泡过程后,最大元素沉淀到最右位置。第二回合,只剩下n-1
个元素,只需要比较n-2次。依次类推,其他比较次数为n-3
,……,2,1. 所以总共比较次数为n*(n-1)/2
。
时间复杂度
最好情况比较次数是O(n)
(有序),最坏情况比较次数O(n*n)
无序。
空间复杂度
O(1)
稳定性
每次都交换一个元素冒泡排序是稳定排序。
Java
例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
35
36
37public class maopaoSort {
void sort(int[] A) {
// last:值最大元素的下标位置
int i, j, last;
i = A.length - 1;
while (i > 0) {
last = 0;
for (j = 0; j < i; j++) {
if (A[j] > A[j + 1]) {
//swap(A[j], A[j + 1]);
int temp = A[j];
A[j] = A[j + 1];
A[j + 1] = temp;
last = j;// last存,值最大元素的下标
}
}
i = last;// last值赋值i,表示last已经在数组最末尾了,每一次比较i的值逐渐变小,直到while循环结束
}
}
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
public static void main(String[] args) {
maopaoSort maopaoSort = new maopaoSort();
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 101, 56, 17, 18, 23, 34, 15,
35, 25, 53, 51 };
maopaoSort.sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + ",");
}
}
}
例2
1 | public class bubbleSort { |
C++
初始状态: [24, 19, 26, 39, 36, 7, 31, 29, 38, 23]
内层第一趟: [24, 19, 26, 39, 36, 7, 31, 29, 23
, 38
] ( [23]<->[38] )
内层第二趟: [24, 19, 26, 39, 36, 7, 31, 23
, 29
, 38] ( [23]<->[29] )
内层第三趟: [24, 19, 26, 39, 36, 7, 23
, 31
, 29, 38] ( [23]<->[31] )
内层第四趟: [24, 19, 26, 39, 36, 7, 23, 31, 29, 38] ( 7 、 23 都位于正确的顺序,无需交换)
内层第五趟: [24, 19, 26, 39, 7
, 36
, 23, 31, 29, 38] ( [7]<->[36] )
内层第六趟: [24, 19, 26, 7
, 39
, 36, 23, 31, 29, 38] ( [7]<->[39] )
内层第七趟: [24, 19, 7
, 26
, 39, 36, 23, 31, 29, 38] ( [7]<->[26] )
内层第八趟: [24, 7
, 19
, 26, 39, 36, 23, 31, 29, 38] ( [7]<->[19] )
内层第九趟: [7
, 24
, 19, 26, 39, 36, 23, 31, 29, 38] ( [7]<->[24] )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/**
* Bubble Sorting, it's very similar with Insertion Sorting
*/
BUBBLE(new Sortable() {
public <T extends Comparable<T>> void sort(T[] array, boolean ascend) {
int length = array.length;
int lastExchangedIdx = 0;
for (int i = 0; i < length; i++) {
// mark the flag to identity whether exchange happened to false
boolean isExchanged = false;
// last compare and exchange happened before reaching index i
int currOrderedIdx = lastExchangedIdx > i ? lastExchangedIdx : i;
for (int j = length - 1; j > currOrderedIdx; j--) {
int compare = array[j - 1].compareTo(array[j]);
if (compare != 0 && compare > 0 == ascend) {
exchange(array, j - 1, j);
isExchanged = true;
lastExchangedIdx = j;
}
}
// if no exchange happen means array is already in order
if (isExchanged == false) {
break;
}
}
}
})
快速排序
基本思想
快速排序也是用归并方法实现的一个“分而治之”的排序算法,它的魅力之处在于它能在每次partition(排序算法的核心所在)都能为一个数组元素确定其排序最终正确位置(下次循环就不考虑这个元素了)。
步骤
- 确定一个元素R(第一个元素是分割元素)是数组分割元素,将数组分割成低端序列(比分割元素值小)和高端序列(比分割元素值大)。
- 用left和right来指向原序列第一个元素和最后一个元素,2个变量i,j作为游动指针,初始化为
i=left,j=right+1
;i从低端序列左边开始向右扫描,找到第1个大于或者等于分割元素的元素,i<j
;则A[i]
赋值A[j]
; j从高端序列右边开始向左扫描,找到第1个小于或者等于分割元素的元素,i<j
;则A[j]
赋值A[i]
;然后,继续次操作,直到i>=j
时,这趟排序结束。原来属于低端的值,放在低端。属于高端的值,在高端。 - 分割元素
A[left]
和A[i]
交换,分割元素回到分割位置,分别对低端序列和高端序列排序。
时间复杂度
初始序列有序,这时候快速排序效率最低平均情况下,最坏情况比较次数O(n*n)
。最优情况是O($nlogN$)。
空间复杂度
最坏情况下O(n)
。
稳定性
进过1趟排序,就可以确认元素的位置,是不稳定排序。如[5,3,3,4,3,8,9,10]
第1次切分,主元5要和元素3交换,即改变了3和另两个相等元素之间的顺序。
规模大小
元素多的情形,适合无序序列,元素较小,还不如其他排序。
优化
分割元素最大值或者最小值,会导致排序效率最低,为了避免这种情况,选取分割元素的时候,可以做如下处理。
A[(left+right)/2]
作为分割元素,与A[left]
进行交换。- 取left和right随机数,与A[left]进行交换。
- 去
A[left]
,A[(left+right)/2]
和A[right]
之间的值,与A[left]
进行交换。Java
例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
35
36
37
38
39
40
41
42package quick;
public class QuickSort {
void sort(int[] A, int left, int right) {
int i = 0, j = 0;
if (left < right) {
i = left;
j = right;
int key = A[left];
// 将小于分割元素放到低端,将大于分割元素放到高端
while (i < j) {
while (i < j && A[j] >= key) {
j--;
}
if (i < j) {
A[i] = A[j];
}
while (i < j && A[i] <= key) {
i++;
}
if (i < j) {
A[j] = A[i];
}
}
A[j] = key;
// 对低端元素进行排序
sort(A, 0, i - 1);
// 对高端元素进行排序
sort(A, j + 1, right);
}
}
public static void main(String[] args) {
QuickSort quickSort = new QuickSort();
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 101, 56, 17, 18, 23, 34, 15,
35, 25, 53, 51 };
quickSort.sort(a, 0, a.length - 1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + ",");
}
}
}
例2
1 | public class quickSort { |
C++
初始(i = 1, lt = 0, gt = 8):[41
, 59, 43, 26, 63, 30, 29, 26, 42](需要确定位置的为[41])
第一趟(i = 1, lt = 0, gt = 8):[41, 42
, 43, 26, 63, 30, 29, 26, 59
](59>41,[59]<->[42],gt–)
第二趟(i = 1, lt = 0, gt = 7):[41, 26
, 43, 26, 63, 30, 29, 42
, 59](42>41,[42]<->[26],gt–)
第三趟(i = 1, lt = 0, gt = 6):[26
, 41
, 43, 26, 63, 30, 29, 42, 59](26<41, [26]<->[41],i++, lt++)
第四趟(i = 2, lt = 1, gt = 6):[26, 41, 29
, 26, 63, 30, 43
, 42, 59](43>41,[43]<->[29],gt–)
第五趟(i = 2, lt = 1, gt = 5):[26, 29
, 41
, 26, 63, 30, 43, 42, 59](29<41, [29]<->[41],i++,lt++)
第六趟(i = 3, lt = 2, gt = 5):[26, 29, 26
, 41
, 63, 30, 43, 42, 59](26<41,[26]<->[41],i++,lt++)
第七趟(i = 4, lt = 3, gt = 5):[26, 29, 26, 41, 30
, 63
, 43, 42, 59] (63>41,[63]<->[30],gt–)
第八趟(i = 4, lt = 3, gt = 4):[26, 29, 26, 30
, 41
, 63, 43, 42, 59](30<41,[30]<->[41],i++,lt++) ->->->->->->->->
可以看出,在一次partition之后,以41为分割线,41左侧皆为比它小的元素,41右侧皆为比它大或相等的元素(当然这个实例比较特殊,没有出现和41相等的元素)。快速排序顾名思义就是排序速度非常快。值得一提的是JDK中在Arrays工具内中内置的sort方法就是接合插入排序和三路快速排序实现的,有兴趣的同学可以看看JDK的源码。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/**
* Quick Sorting
*/
QUICK(new Sortable() {
public <T extends Comparable<T>> void sort(T[] array, boolean ascend) {
this.sort(array, 0, array.length - 1, ascend);
}
private <T extends Comparable<T>> void sort(T[] array, int lo, int hi, boolean ascend) {
if (lo >= hi) {
return;
}
T toFinal = array[lo];
int leftIdx = lo;
int rightIdx = hi;
int i = lo + 1;
while (i <= rightIdx) {
int compare = array[i].compareTo(toFinal);
if (compare == 0) {
i++;
} else if (compare < 0 == ascend) {
exchange(array, leftIdx++, i++);
} else {
exchange(array, rightIdx--, i);
}
}
// partially sort left array and right array
// no need to include the leftIdx-th to rightIdx-th elements
// since they are already in its final position
sort(array, lo, leftIdx - 1, ascend);
sort(array, rightIdx + 1, hi, ascend);
}
})
两路合并排序
基本思想
归并排序采用的是递归来实现,属于“分而治之”,将目标数组从中间一分为二,之后分别对这两个数组进行排序,排序完毕之后再将排好序的两个数组“归并”到一起,归并排序最重要的也就是这个“归并”的过程。
将N个元素的数组,看成N个长度为1的子序列,然后进行两两合并子序列,得到N/2
个长度为2的有序子序列,然后不停的两两2合并,新创建一个临时数组,来存放临时排序的数据,定义子序列的上界和下界。
时间复杂度
最差情况O(n$\log{N}$),最好情况O($\log{N}$)
空间复杂度
O(n)
稳定性
将待排序列递归地划分为短序列,指导每部分都只包含一个元素,然后再合并,合并时如果两个元素相等也会按照元素之前的顺序,把下标小的元素先放入结果列表中,依然没有破环相同元素之间原本的顺序,因此归并算法也是稳定排序。
Java
例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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63package merge;
public class MergeSort {
/**
* 子序列1的上界:i1;下界:j1
* 子序列1的上界:i2,下界:j2
* 2个子序列是相邻的,所以:i2=j1+1
* @param A
* @param i1 子序列1上界
* @param j1 子序列1下界
* @param i2 子序列2上界
* @param j2 子序列2下界
*/
void merge(int[] A, int i1, int j1, int i2, int j2) {
// 创建一个临时数组,来存取子序列
int[] temp = new int[A.length];
// i,j是子序列的游动指针,k是临时数组游动指针
int i = i1, j = i2, k = 0;
// 将2个子序列中最小值,存入临时数组
while (i <= j1 && j <= j2) {
if (A[i] <= A[j]) {
temp[k++] = A[i++];
} else {
temp[k++] = A[j++];
}
}
//第一个序列还有剩余元素,再存入temp
while (i <= j1) {
temp[k++] = A[i++];
}
//第二个序列还有剩余元素,再存入temp
while (j <= j2) {
temp[k++] = A[j++];
}
// 将临时数组值赋值给原数组A
for (int k2 = 0; k2 < k; k2++) {
A[i1++] = temp[k2];
}
}
public void sort(int[] A, int left, int right) {
if(left<right){
// 找出中间索引
int center = (left + right) / 2;
// 对左边数组进行递归
sort(A, left, center);
// 对右边数组进行递归
sort(A, center + 1, right);
merge(A, left, center, center + 1, right);
}
}
public static void main(String[] args) {
MergeSort mergeSort = new MergeSort();
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 101, 56, 17, 18, 23, 34, 15,
35, 25, 53, 51 };
mergeSort.sort(a, 0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + ",");
}
}
}
例2
1 | import java.util.Arrays; |
C++
归并的过程中需要额外的跟需要归并的两个数组长度一致的空间(另外开辟一个数组空间),比如需要规定的数组分别为:[3, 6, 8, 11]
和[1, 3, 12, 15]
(虽然逻辑上被划为为两个数组,但实际上这些元素还是位于原来数组中的,只是通过一些index将其划分成两个数组,原数组为[3, 6, 8, 11, 1, 3, 12, 15]
,我们设置三个指针lo
,mid
,high
分别为0,3,7
就可以实现逻辑上的子数组划分)那么需要的额外数组的长度为4 + 4 = 8
。归并的过程可以简要地概括为如下。
- 将两个子数组中的元素复制到新
copiedArray[]
中,以前面提到的例子为例,则copiedArray=[3, 6, 8, 11, 1, 3, 12, 15]
; - 设置两个指针分别指向原子数组中对应的第一个元素,假定这两个指针取名为
leftIdx
和rightIdx
,则leftIdx=0
(对应copiedArray中的第一个元素[3]
),rightIdx=4
(对应copiedArray中的第五个元素[1]
); - 比较
leftIdx
和rightIdx
指向的数组元素值,选取其中较小的一个并将其值赋给原数组中对应的位置i,赋值完毕后分别对参与赋值的这两个索引做自增1操作,如果leftIdx
或rigthIdx
值已经达到对应数组的末尾,则余下只需要将剩下数组的元素按顺序copy到余下的位置即可。
例:
第一趟:辅助数组[21, 28, 39 | 35, 38]
(数组被拆分为左右两个子数组,以 | 分隔开)[21 , , , , ]
(第一次 21 与 35 比较 , 左边子数组胜出,leftIdx = 0, i = 0
)
第二趟:辅助数组[21, 28 , 39 | 35, 38]
[21 , 28, , , ]
(第二次 28 与 35 比较,左边子数组胜出,leftIdx = 1, i = 1
)
第三趟:[21, 28, 39 | 35 , 38]
[21 , 28 , 35, , ]
(第三次 39 与 35 比较,右边子数组胜出,rightIdx = 0, i = 2
)
第四趟:[21, 28, 39 | 35, 38 ]
[21 , 28 , 35 , 38, ]
(第四次 39 与 38 比较,右边子数组胜出,rightIdx = 1, i = 3
)
第五趟:[21, 28, 39 | 35, 38]
[21 , 28 , 35 , 38 , 39]
(第五次时右边子数组已复制完,无需比较leftIdx = 2, i = 4
)
以上便是一次归并的过程,我们可以将整个需要排序的数组做有限次拆分(每次一分为二)直到分为长度为1的小数组为止,长度为1时数组已经不用排序了。在这之后再逆序(由于采用递归)依次对这些数组进行归并操作,直到最后一次归并长度为n/2
的子数组,归并完成之后数组排序也完成。
归并排序需要的额外空间是所有排序中最多的,每次归并需要与参与归并的两个数组长度之和相同个元素(为了提供辅助数组)。则可以推断归并排序的空间复杂度为1+2+4+…+n=n*(n+2)/4
(忽略了n的奇偶性的判断)。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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63/**
* Merge sorting
*/
MERGE(new Sortable() {
public <T extends Comparable<T>> void sort(T[] array, boolean ascend) {
this.sort(array, 0, array.length - 1, ascend);
}
private <T extends Comparable<T>> void sort(T[] array, int lo, int hi, boolean ascend) {
// OPTIMIZE ONE
// if the substring's length is less than 20,
// use insertion sort to reduce recursive invocation
if (hi - lo < 20) {
for (int i = lo + 1; i <= hi; i++) {
T toInsert = array[i];
int j = i;
for (; j > lo; j--) {
int compare = array[j - 1].compareTo(toInsert);
if (compare == 0 || compare < 0 == ascend) {
break;
}
array[j] = array[j - 1];
}
array[j] = toInsert;
}
return;
}
int mid = lo + (hi - lo) / 2;
sort(array, lo, mid, ascend);
sort(array, mid + 1, hi, ascend);
merge(array, lo, mid, hi, ascend);
}
private <T extends Comparable<T>> void merge(T[] array, int lo, int mid, int hi, boolean ascend) {
// OPTIMIZE TWO
// if it is already in right order, skip this merge
// since there's no need to do so
int leftEndCompareToRigthStart = array[mid].compareTo(array[mid + 1]);
if (leftEndCompareToRigthStart == 0 || leftEndCompareToRigthStart < 0 == ascend) {
return;
}
@SuppressWarnings("unchecked")
T[] arrayCopy = (T[]) new Comparable[hi - lo + 1];
System.arraycopy(array, lo, arrayCopy, 0, arrayCopy.length);
int lowIdx = 0;
int highIdx = mid - lo + 1;
for (int i = lo; i <= hi; i++) {
if (lowIdx > mid - lo) {
// left sub array exhausted
array[i] = arrayCopy[highIdx++];
} else if (highIdx > hi - lo) {
// right sub array exhausted
array[i] = arrayCopy[lowIdx++];
} else if (arrayCopy[lowIdx].compareTo(arrayCopy[highIdx]) < 0 == ascend) {
array[i] = arrayCopy[lowIdx++];
} else {
array[i] = arrayCopy[highIdx++];
}
}
}
})
基数排序
基本思想
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。N个元素进行d趟排序,每趟分配的元素放到r个队列中,需要收集N个元素,这样每趟分配和收集时间需要O(N+r)
,d趟需要时间O(d*(r+N))
。
时间复杂度
O(d*(r+N))
空间复杂度
(N+2*r)
稳定排序
等到排序之后才能确定元素的位置,是稳定排序。
Java
例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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73package base;
import java.util.ArrayList;
import java.util.List;
/**
* 基准排序
*
*/
public class BaseSort {
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 101, 56, 17, 18, 23, 34, 15, 35,
25, 53, 51 };
public static void main(String[] args) {
new BaseSort();
}
public BaseSort() {
sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+", ");
}
}
public void sort(int[] array) {
// 首先确定排序的趟数;
// 最大值
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int time = 0;
// 判断位数;
while (max > 0) {
max /= 10;
time++;
}
// 建立10个队列;
List<ArrayList> queue = new ArrayList<ArrayList>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
// 进行time次分配和收集;
for (int i = 0; i < time; i++) {
// 分配数组元素;
for (int j = 0; j < array.length; j++) {
// 得到数字的第time+1位数;
int x = array[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
// 位数队列
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(array[j]);
queue.set(x, queue2);
}
int count = 0;// 元素计数器;
// 收集队列元素;
// 重新给array排序
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
ArrayList<Integer> queue3 = queue.get(k);
array[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
例2
1 | import java.util.ArrayList; |