Sorting1 - Comparison based
Bubble Sort
算法
第一轮从第一个元素开始,两两比较,如果左大右小则交换,这样一轮下来,最右边的值最大,和名字里的冒泡一样;第二轮从第一个到第 n − 1 n-1 n−1个,依此类推。
代码
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
复杂度
时间复杂度:没有比较可以省略。
Best case:
O
(
n
2
)
O(n^2)
O(n2)
Worst case:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
Selection Sort
算法
先找到从左到右的最小元素,交换到
n
u
m
s
[
0
]
nums[0]
nums[0]的位置;第二次遍历总左侧第二到最右找到最小元素,交换到
n
u
m
s
[
1
]
nums[1]
nums[1]的位置;重复直到全部元素排序。
因为涉及到远距离交换所以不稳定。
代码
public static void selectionSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int min = Integer.MAX_VALUE;
int index = 0;
for (int j = i; j < arr.length; j++) {
if (arr[j] < min) {
min = arr[j];
index = j;
}
}
int temp = arr[i];
arr[i] = min;
arr[index] = temp;
}
}
复杂度
时间复杂度:任何情况都需要遍历所有元素寻找最小。
Best case:
O
(
n
2
)
O(n^2)
O(n2)
Worst case:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
Insertion Sort
算法
在数列中维持一个结构,index < i
的数已经完成排序,这时将
n
u
m
s
[
i
+
1
]
nums[i+1]
nums[i+1]不断与左边的数比较,找到比左边数大的位置即停止。
例子:
0
,
3
,
4
∣
1
,
5
,
7
0,3,4|1,5,7
0,3,4∣1,5,7,
i
=
3
i = 3
i=3
1
<
4
1 < 4
1<4则交换 ->
0
,
3
,
1
∣
4
,
5
,
7
0,3,1|4,5,7
0,3,1∣4,5,7
1
<
3
1 < 3
1<3则交换 ->
0
,
1
,
3
∣
4
,
5
,
7
0,1,3|4,5,7
0,1,3∣4,5,7
1
>
0
1 > 0
1>0则停止,把隔板右移(i++
) ->
0
,
1
,
3
,
4
∣
5
,
7
0,1,3,4|5,7
0,1,3,4∣5,7
代码
做了一点点更改,没有一直交换,记录下要插入的数字,顺移左侧需要移动的数字,最后把要插入的数字放在对应的位置。
public static void insertionSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i; j >= 1 && arr[j - 1] > temp; j--) {
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
复杂度
时间复杂度:
Best case:
O
(
n
)
O(n)
O(n),数组本来已经排序时每个数只需要往左比较一次即停止。
Worst case:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
Heap Sort
算法
利用最大堆的数据结构特点,每个节点的值都大于两个子节点,排序就是把所有数放进堆,再一直取出顶端值。
堆可以通过数组进行表示,
n
u
m
s
[
i
]
nums[i]
nums[i]的两个子节点为
n
u
m
s
[
2
i
+
1
]
nums[2i + 1]
nums[2i+1]和
n
u
m
s
[
2
i
+
2
]
nums[2i+2]
nums[2i+2]
加入元素时,比较
n
u
m
s
[
i
]
nums[i]
nums[i]和子节点的大小,如果小的话则往下交换(保持堆的性质)。
去除最大元素时,将根节点和最后一个叶节点交换,根节点再向下交换,重新达到最大堆的条件。
代码
public static void heapSort(int[] arr) {
for (int i = arr.length / 2 - 1; i >= 0; i--) { // all nodes that are not leaves
bubbleDown(arr, i, arr.length);
}
for (int i = arr.length - 1; i >= 0; i--) {
int temp = arr[i];
arr[i] = arr[0]; // arr[0] is the biggest, exchange it to the back
arr[0] = temp;
bubbleDown(arr, 0, i); // arr length is i since arr[i] was thought as off-heap now
}
}
public static void bubbleDown(int[] arr, int i, int len) {
int index = i;
if (2 * i + 1 < len && arr[index] < arr[2 * i + 1]) {
index = 2 * i + 1;
}
if (2 * i + 2 < len && arr[index] < arr[2 * i + 2]) {
index = 2 * i + 2;
}
if (index != i) {
// exchange with the bigger one
int temp = arr[index];
arr[index] = arr[i];
arr[i] = temp;
bubbleDown(arr, index, len); // e.g. 1 -> 2 -> 4 to 2 -> 1 -> 4 so recur on 1 again
}
}
复杂度
时间复杂度:
Best case:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
Worst case:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度: O ( 1 ) O(1) O(1) (bubble down操作只是对数组进行In-place操作)
Merge Sort
算法
分治法的应用。
每次将数组对半分,分别进行排序,base case为数组长度为0或1时直接返回。
代码
public static void mergeSort(int[] arr, int l, int r) {
if (l >= r) {
return;
}
int m = (l + r) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
public static void merge(int[] arr, int l, int m, int r) {
int[] left = Arrays.copyOfRange(arr, l, m + 1);
int[] right = Arrays.copyOfRange(arr, m + 1, r + 1);
int i = 0, j = 0, k = l;
while (i < left.length && j < right.length) {
if (left[i] <= right[j])
arr[k++] = left[i++];
else {
arr[k++] = right[j++];
}
}
while (i < left.length)
arr[k++] = left[i++];
while (j < right.length)
arr[k++] = right[j++];
}
复杂度
时间复杂度:根据递归树深度
Worst case:
O
(
l
o
g
n
)
O(logn)
O(logn)
Best case:
O
(
l
o
g
n
)
O(logn)
O(logn)
空间复杂度:
O
(
n
)
=
O
(
n
)
+
O
(
l
o
g
n
)
O(n) = O(n) + O(logn)
O(n)=O(n)+O(logn)(recursion)
不是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)是因为实际上计算是并行发生的,参考以下链接:
merge-sort-time-and-space-complexity
QuickSort
算法
选择一个数作为pivot,遍历数组,把比pivot小的放在一遍,比pivot大的放另外一边,再对大小两个数组递归排序。当pivot选择为中位数时,相当于In-place MergeSort.
例子:
5
,
7
,
0
,
6
,
1
,
9
5,7,0,6,1,9
5,7,0,6,1,9,
p
i
v
o
t
=
5
(
i
n
d
e
x
=
0
)
pivot = 5 (index = 0)
pivot=5(index=0)
Step1. 交换
n
u
m
s
[
0
]
nums[0]
nums[0]和
n
u
m
s
[
i
n
d
e
x
]
nums[index]
nums[index]
5
,
7
,
0
,
6
,
1
,
9
5,7,0,6,1,9
5,7,0,6,1,9
Step2. 初始化
i
=
1
i = 1
i=1,
i
i
i为小于和等于分界,
j
j
j为等于和大于分界。
遍历
j
=
1
,
2
,
.
.
.
j=1, 2, ...
j=1,2,...
j
=
1
j=1
j=1,放数字
7
7
7
5
∣
7
,
0
,
6
,
1
,
9
5|7,0,6,1,9
5∣7,0,6,1,9,
i
=
1
i = 1
i=1
j
=
2
j=2
j=2,放数字
0
0
0
5
,
0
∣
7
,
6
,
1
,
9
5,0|7,6,1,9
5,0∣7,6,1,9,
i
=
2
i = 2
i=2
j
=
3
j=3
j=3,放数字
6
6
6
5
,
0
∣
7
,
6
,
1
,
9
5,0|7,6,1,9
5,0∣7,6,1,9,
i
=
2
i = 2
i=2
j
=
4
j=4
j=4,放数字
1
1
1
5
,
0
,
1
∣
6
,
7
,
9
5,0,1|6,7,9
5,0,1∣6,7,9,
i
=
3
i = 3
i=3
j
=
5
j=5
j=5,放数字
9
9
9
5
,
0
,
1
∣
6
,
7
,
9
5,0,1|6,7,9
5,0,1∣6,7,9,
i
=
3
i = 3
i=3
Step3. 交换
n
u
m
s
[
0
]
nums[0]
nums[0]和
n
u
m
s
[
i
−
1
]
nums[i-1]
nums[i−1]
Step4. 递归排序子序列
常见pivot选择:
- 选择第一个元素,但在已经排序时不能很好的分割数据表现较差
- 随机选择
- 取第一个,最后一个,中间一个的中位数
代码
public class QuickSort {
public static void main(String[] args) {
int[] test = new int[]{5, 7, 0, 6, 1, 9};
Utils.printarr(test);
quicksort(test, 0, test.length - 1);
Utils.printarr(test);
}
public static void quicksort(int[] arr, int l, int r) {
// System.out.println(l+" "+r);
if (l >= r) {
return;
}
int pivot = arr[l];
int i = l + 1;
for (int j = l + 1; j <= r; j++) {
if (arr[j] < pivot) {
swap(arr, i, j);
i++;
}
}
swap(arr, l, i - 1);
quicksort(arr, l, i - 2);
quicksort(arr, i, r);
}
public static void swap(int[] arr, int l, int r) {
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
}
Output:
5 7 0 6 1 9
0 1 5 6 7 9
复杂度
参考:quicksort
时间复杂度:
Worst case:
O
(
n
2
)
O(n^2)
O(n2) (数组已经排序,pivot一直选第一个数)
Best case:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
Average case:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
Worst case:
O
(
1
)
O(1)
O(1)
Best case:
O
(
l
o
g
n
)
O(logn)
O(logn)
由递归深度决定。
3-Way QuickSort
算法
普通的Quicksort在处理有很多重复元素的情况下效果不佳,所以3-Way的想法是添加一类,与pivot相等的元素集合。
在QuickSort时记录一个大小分界
i
i
i,不断把比pivot小的数从
i
i
i右侧搬到
i
i
i左侧。
算法更改为记录两个分界,一个记录小于和等于的分界,一个记录等于和大于的边界。
例子:
0
,
7
,
5
,
1
,
5
,
9
,
3
,
5
,
5
0,7,5,1,5,9,3,5,5
0,7,5,1,5,9,3,5,5,
p
i
v
o
t
=
5
(
i
n
d
e
x
=
2
)
pivot = 5 (index = 2)
pivot=5(index=2)
Step1. 交换
n
u
m
s
[
0
]
nums[0]
nums[0]和
n
u
m
s
[
i
n
d
e
x
]
nums[index]
nums[index]
0
,
7
,
5
,
1
,
5
,
9
,
3
,
5
,
5
0,7,5,1,5,9,3,5,5
0,7,5,1,5,9,3,5,5
Step2. 初始化
i
=
j
=
1
i = j = 1
i=j=1,
i
i
i为小于和等于分界,
j
j
j为等于和大于分界。
遍历
k
=
1
,
2
,
.
.
.
k=1, 2, ...
k=1,2,...
k
=
1
k=1
k=1,放数字
7
7
7
5
∣
∣
7
,
0
,
1
,
5
,
9
,
3
,
5
,
5
5||7,0,1,5,9,3,5,5
5∣∣7,0,1,5,9,3,5,5,
i
=
1
,
j
=
1
i = 1, j = 1
i=1,j=1
k
=
2
k=2
k=2,放数字
0
0
0
5
,
0
∣
∣
7
,
1
,
5
,
9
,
3
,
5
,
5
5,0||7,1,5,9,3,5,5
5,0∣∣7,1,5,9,3,5,5,
i
=
2
,
j
=
2
i = 2, j = 2
i=2,j=2
k
=
3
k=3
k=3,放数字
1
1
1
5
,
0
,
1
∣
∣
7
,
5
,
9
,
3
,
5
,
5
5,0,1||7,5,9,3,5,5
5,0,1∣∣7,5,9,3,5,5,
i
=
3
,
j
=
3
i = 3, j = 3
i=3,j=3
k
=
4
k=4
k=4,放数字
5
5
5
5
,
0
,
1
∣
5
∣
7
,
9
,
3
,
5
,
5
5,0,1|5|7,9,3,5,5
5,0,1∣5∣7,9,3,5,5,
i
=
3
,
j
=
4
i = 3, j = 4
i=3,j=4
k
=
5
k=5
k=5,放数字
9
9
9
5
,
0
,
1
∣
5
∣
7
,
9
,
3
,
5
,
5
5,0,1|5|7,9,3,5,5
5,0,1∣5∣7,9,3,5,5,
i
=
3
,
j
=
4
i = 3, j = 4
i=3,j=4
k
=
6
k=6
k=6,放数字
3
3
3
5
,
0
,
1
,
3
∣
5
∣
7
,
9
,
5
,
5
5,0,1,3|5|7,9,5,5
5,0,1,3∣5∣7,9,5,5,
i
=
4
,
j
=
5
i = 4, j = 5
i=4,j=5
k
=
7
k=7
k=7,放数字
5
5
5
5
,
0
,
1
,
3
∣
5
,
5
∣
9
,
7
,
5
5,0,1,3|5,5|9,7,5
5,0,1,3∣5,5∣9,7,5,
i
=
4
,
j
=
6
i = 4, j = 6
i=4,j=6
k
=
8
k=8
k=8,放数字
5
5
5
5
,
0
,
1
,
3
∣
5
,
5
,
5
∣
7
,
9
5,0,1,3|5,5,5|7,9
5,0,1,3∣5,5,5∣7,9,
i
=
4
,
j
=
7
i = 4, j = 7
i=4,j=7
Step3. 交换
n
u
m
s
[
0
]
nums[0]
nums[0]和
n
u
m
s
[
i
−
1
]
nums[i-1]
nums[i−1]
3
,
0
,
1
,
5
∣
5
,
5
,
5
∣
7
,
9
3,0,1,5|5,5,5|7,9
3,0,1,5∣5,5,5∣7,9
分割板
i
i
i减小1后得
3
,
0
,
1
∣
5
,
5
,
5
,
5
∣
7
,
9
3,0,1|5,5,5,5|7,9
3,0,1∣5,5,5,5∣7,9
Step4. 递归排序子序列
代码
public class QuickSort {
public static void main(String[] args) {
int[] test = new int[]{5,7,0,1,5,9,3,5,5};
Utils.printarr(test);
threeWayQuicksort(test, 0, test.length - 1);
Utils.printarr(test);
}
public static void threeWayQuicksort(int[] arr, int l, int r) {
if (l >= r) {
return;
}
int pivot = arr[l];
int i = l + 1, j = l + 1;
for (int k = l + 1; k <= r; k++) {
if (arr[k] == pivot) {
swap(arr, j, k);
j++;
} else if (arr[k] < pivot) {
swap(arr, j, k);
swap(arr, i, j);
i++;
j++;
}
}
swap(arr, l, i - 1);
threeWayQuicksort(arr, l, i - 2);
threeWayQuicksort(arr, j, r);
}
public static void swap(int[] arr, int l, int r) {
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
}
Output:
5 7 0 1 5 9 3 5 5
0 1 3 5 5 5 5 7 9
复杂度
时间复杂度:
Average:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
Average:
O
(
l
o
g
n
)
O(logn)
O(logn)
Shell Sort
参考:
https://en.wikipedia.org/wiki/Shellsort
算法
可以看作一个分组的Insertion Sort。
先是一些符号定义:
g
a
p
gap
gap
s
e
q
u
e
n
c
e
sequence
sequence: 一列递减的数,值为每轮的gap,最后一个数为1
h
−
s
o
r
t
h-sort
h−sort: 对任意
i
i
i,子列
{
a
i
,
a
i
+
h
,
a
i
+
2
h
,
.
.
.
}
\{a_{i}, a_{i+h}, a_{i+2h}, ...\}
{ai,ai+h,ai+2h,...}有序
排序过程:
从
g
a
p
gap
gap
s
e
q
u
e
n
c
e
sequence
sequence中依次选择
g
a
p
=
h
gap = h
gap=h,对
{
a
i
,
a
i
+
h
,
a
i
+
2
h
,
.
.
.
}
\{a_{i}, a_{i+h}, a_{i+2h}, ...\}
{ai,ai+h,ai+2h,...}进行插入排序,直至
g
a
p
=
1
gap = 1
gap=1时全部排序。该排序的大意是首先通过长距离的交换,来减少后续短距离交换时的逆序数。
常用的 g a p gap gap s e q u e n c e sequence sequence:
- 2 k + 1 2^k + 1 2k+1
- 4 k + 3 ⋅ 2 k − 1 + 1 {4^{k}+3\cdot 2^{k-1}+1} 4k+3⋅2k−1+1
- 1 , 4 , 10 , 23 , 57 , 132 , 301 , 701 1, 4, 10, 23, 57, 132, 301, 701 1,4,10,23,57,132,301,701 (From experiment, no formula)
代码
public class ShellSort {
public static void main(String[] args) {
int[] test = new int[]{1, 4, 10, 3, 5, 17, 7, 2, 6, 8, 20, 15, 32, 9, 13};
sort(test, new int[] {10, 4, 1});
for (int i : test) {
System.out.print(i + " ");
}
}
public static void sort(int[] nums, int[] gapSeq) {
for (int h : gapSeq) {
hsort(nums, h);
}
}
public static void hsort(int[] nums, int h) {
int temp;
for (int i = 0; i < h; i++) { // h subsequences to sort
for (int j = i + h; j < nums.length; j += h) { // go through numbers in subsequence
temp = nums[j];
int k;
for (k = j; k >= h && temp < nums[k - h]; k -= h) {
nums[k] = nums[k - h]; // shift right
}
nums[k] = temp; // insert nums[j]
}
}
}
}
Output: 1 2 3 4 5 6 7 8 9 10 13 15 17 20 32
算法正确性
定理:一个
h
−
s
o
r
t
h-sort
h−sort的子列在进行
k
−
s
o
r
t
k-sort
k−sort后依然是一个
h
−
s
o
r
t
h-sort
h−sort子列。
反证法:假设原来
a
i
<
a
i
+
h
a_{i} < a_{i + h}
ai<ai+h,
k
−
s
o
r
t
k-sort
k−sort结束变成了
a
i
′
>
a
i
+
h
′
a'_{i} > a'_{i + h}
ai′>ai+h′,
如果是假设
a
i
=
a
i
′
a_{i} = a'_{i}
ai=ai′,
a
i
+
h
a_{i + h}
ai+h发生了变化,有两种可能,
第一种它插入到了前面
a
i
+
h
−
n
k
a_{i + h - nk}
ai+h−nk的位置上,此时就有
a
i
+
h
=
a
i
+
h
−
n
k
′
<
a
i
+
h
′
<
a
i
′
=
a
i
a_{i + h} = a'_{i + h - nk} < a'_{i + h} < a'_{i} = a_{i}
ai+h=ai+h−nk′<ai+h′<ai′=ai,矛盾;
第二种是它被后面的
a
i
+
h
+
m
k
a_{i+h+mk}
ai+h+mk插入到了当前位置,此时有
a
i
+
h
+
m
k
=
a
i
+
h
′
a_{i+h+mk} = a'_{i + h}
ai+h+mk=ai+h′,
a
i
a_{i}
ai不动说明
a
i
<
a
i
+
n
k
a_{i} < a_{i+nk}
ai<ai+nk对任意
n
n
n成立(否则被插入),
则有
a
i
+
n
k
>
a
i
=
a
i
′
>
a
i
+
h
′
=
a
i
+
h
+
m
k
a_{i+nk} > a_{i} = a'_{i} > a'_{i + h} = a_{i+h+mk}
ai+nk>ai=ai′>ai+h′=ai+h+mk,
取
n
=
m
n = m
n=m,得到原来数组不为
h
−
s
o
r
t
h-sort
h−sort,矛盾。
a
i
a_{i}
ai发生变化的情况类似。
由以上定理可知最后数组为 1 − s o r t 1-sort 1−sort子列,即已排序。
复杂度
依赖于 g a p gap gap s e q u e n c e sequence sequence的选择。
时间复杂度:(和Insertion一样)
Best case:
O
(
n
)
O(n)
O(n)
Worst case:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
总结
名称 | Best | Worst | Stable | Memory |
---|---|---|---|---|
Bubble Sort | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | Y | O ( 1 ) O(1) O(1) |
Selection Sort | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | N | O ( 1 ) O(1) O(1) |
Insertion Sort | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | Y | O ( 1 ) O(1) O(1) |
Heap Sort | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | N | O ( 1 ) O(1) O(1) |
Merge Sort | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | Y | O ( n ) O(n) O(n) |
QuickSort | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | N | O ( l o g n ) O(logn) O(logn) |
Shell Sort | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | N | O ( 1 ) O(1) O(1) |