排序-归并排序 | 8lovelife's life
0%

排序-归并排序

怎么对数据进行排序?排序算法有多种,该使用哪种排序算法呢?回顾下时间复杂度较低的归并排序

归并排序

归并排序的核心分为两个部分。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
public void mergeSortByRecursive(int[] data) {
long startTime = System.nanoTime();
mergeSortRecursive(0, data.length - 1, data, new int[data.length]);
System.out.println("MergeSortRecursive Time Cost : " + (System.nanoTime() - startTime) * 0.000001 +" ms");
checkSortedData(data,"MergeSortRecursive");
}

private void mergeSortRecursive(int lowIndex, int highIndex, int[] data, int[] temp) {
if (lowIndex >= highIndex) {
return;
}
int mid = ((highIndex - lowIndex) >> 1) + lowIndex;
int startOne = lowIndex, endOne = mid, startTwo = mid + 1, endTwo = highIndex;
mergeSortRecursive(startOne, endOne, data, temp);
mergeSortRecursive(startTwo, endTwo, data, temp);
mergeData(lowIndex, mid, highIndex, data, temp);
}

private void mergeData(int lowIndex, int middle, int highIndex, int[] data, int[] temp) {

for (int i = lowIndex; i <= highIndex; i++) {
temp[i] = data[i];
}

int leftStart = lowIndex, rightStart = middle + 1;
int tempIndex = leftStart;
while (leftStart <= middle && rightStart <= highIndex) {
data[tempIndex++] = temp[leftStart] < temp[rightStart] ? temp[leftStart++] : temp[rightStart++];
}

while (leftStart <= middle) {
data[tempIndex++] = temp[leftStart++];
}

}

递归归并排序示意图

image

上述的排序代码是理想状态下的,当排序的数据量较大的时候,1。使用递归的方式对数据进行分治,排序的过程将不能正常结束,递归达到一定的深度将导致栈内存溢出。2。merge排序过程中需要额外的存储空间

归并排序(迭代方式)

迭代归并排序将不会出现栈内存溢出的问题,同时对于merge部分可以采用原地排序,降低空间复杂度

代码实现

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
private void mergeSortByIterative(int[] data) {

for ( int block=1; block<=data.length-1/2; block = 2*block)
{
for (int left_start=0; left_start<data.length-1; left_start += 2*block)
{
int mid = left_start + block - 1 < data.length-1 ? left_start + block - 1 : left_start-1;
int right_end = (left_start + 2 * block - 1) < data.length - 1 ? left_start + 2 * block - 1 : data.length-1;
mergeDataInPlace(left_start,mid,right_end,data);

}
}

}

private void mergeDataInPlace(int lowIndex, int middle, int highIndex, int[] data) {

int i = lowIndex;
int j = middle + 1;
while(i < j && j <= highIndex)
{
while(i < j && data[i] <= data[j])
{
i++;
}
int index = j;
while(j <= highIndex && data[j] <= data[i])
{
j++;
}
swap(data, i, index-i, j-index);
i += (j-index);
}
}

private void swap(int arr[], int i, int k, int j) {
reverse(arr, i, i + k - 1);
reverse(arr, i + k, i + k + j - 1);
reverse(arr, i, i + k + j - 1);
}

private void reverse(int[] arr, int i, int j) {
while(i < j)
{
arr[i] ^= arr[j];
arr[j] ^= arr[i];
arr[i] ^= arr[j];
i++;
j--;
}
}

迭代归并排序示意图

image

归并的算法复杂度

  1. 将N个待排序的数据对半分,分到剩下单个元素为止,这个过程需要log2N
  2. 针对两部分有序的数据进行排序,比较的次数介于 N ~ N/2 之间
  • 时间复杂度:O(Nlog2N)
  • 空间复杂度:O(1) ~ O(N)

实际运行效果

时间与空间不可兼得

来看看归并排序具体所需的时间,测试数据为0~20W的随机大小数字,20W个数据排序5次

  • 迭代归并排序(归并部分采用In-Place)
排序次数 所需时间
1 20093.858586 ms
2 19864.726974999998 ms
3 19930.830952 ms
4 19667.228037999997 ms
5 19873.263419 ms
  • 迭代归并排序(归并部分不采用In-Place)
排序次数 所需时间
1 69.183775 ms
2 45.25653 ms
3 36.282312 ms
4 38.270578 ms
5 35.130196999999995 ms

merge部分算法不同,排序的整体时间天壤之别。。。In-Place merge虽然节省了空间,但是带来了更多的元素移动,更多的时间消耗。时间复杂度与空间复杂度不可兼得

Fork/Join排序

来看看对千万级的数据量进行排序,时间情况如何。测试数据1000W

  • 迭代归并排序
排序次数 所需时间
1 2690.39711 ms
2 2267.34402 ms
3 2423.867487 ms
4 2543.747986 ms
5 2409.55724 ms

归并排序先把大量的数据分为多个小量的数据(分治),然后合并小量的数据(合并),这不是在说Fork/Join的嘛,而且Fork/Join可以利用多核,那么利用Fork/Join排序会不会快一点?

  • Fork/Join归并排序
排序次数 所需时间
1 10921.404759 ms
2 16899.215443 ms
3 14746.151189 ms
4 14887.626859999998 ms
5 15565.447017999999 ms

不敢相信的速度,Fork/Join排序会消耗大量的内存空间,导致频繁的GC,归并排序分治的最小任务是1,Fork/Join虽能并行的处理排序,但任务如果分的过小,会带来更多的线程切换,相比单线程的迭代归并排序,反而需要更多的时间

I’m not a devil, I’m just a good man, put in a bad place. - Verbal Kint

The Usual Suspects