目录
1. 排序的概念及引用
1.1 排序的概念
排序:顾名思义,就是将一组数据,按照一定的顺序 (递增 / 递减) 排列起来,比如 32145 排升序就是 12345,排降序就是 54321。
稳定性:假如一组数据中,有两个或以上的元素,而在排完序后,这两个或以上的元素的先后顺序没变,则说明这个排序是稳定的,否则就是不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。(在硬盘上的排序)
1.2 常见的排序算法
常见的排序算法有:直接插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序,归并排序。
2. 常见排序算法的实现
2.1 插入排序
2.1.1 基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
就比如说线下打扑克牌的时候,当你手上只有一张牌的时候,不需要排序,而你再摸到第二张牌时,就需要与之前的牌比较,按一定的顺序来整理手牌。
2.1.2 直接插入排序
就跟我刚刚说的一样,只有一个数据的时候不需要排序,而当插入第二个或以上的数据时,就需要与前面的数据做比较,这种一边插入一边排序的排序方法就叫做直接插入排序。
思路总结:排升序,从 1 下标开始,往前面比较,遇到比我大的,就让大的往后挪,然后继续向前,遇到比我小的,我就插到比我小的后一位,如果我前面没人了,我就站到第一位。
有了以上思路,我们就可以写代码了。
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j = i - 1;
for (; j >= 0; j--) {
if (arr[j] > tmp) {
// 比 i 大,往后挪
arr[j + 1] = arr[j];
} else {
// 比 i 小,放后一个位置
// arr[j + 1] = tmp;
// 说明前面的元素都有序了
break;
}
}
arr[j + 1] = tmp;
// 如果 i 之前的元素都比i要大,
// 那么 i 就应该放到最前面
// 此时 j 一直--,j 则会变成 -1
}
}
写完之后来测试一下。
public class Test {
public static void main(String[] args) {
int[] arr = {3, 2, 1, 5, 4, 4};
Sort.insertSort(arr);
System.out.println(Arrays.toString(arr));
}
}
没问题,我们再来看看直接插入排序的时间复杂度。
从刚刚的流程图我们可以发现,稳定的排序是可以实现为不稳定的排序的。
只要把条件改为 arr[j] >= tmp,那就是不稳定的。
但是,一个本身就不稳定的排序是不可能实现为稳定的排序的。
直接插入排序总结:
时间复杂度: O(N^2)
空间复杂度:O(1)
稳定性:稳定
数据越有序,排序效率越高。
2.1.3 希尔排序 (缩小增量排序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数 gap,把待排序文件中所有记录分成多个组,所有距离为 gap 的记录分在同一组内,并对每一组内的记录进行排序。然后,缩小 gap ,重复上述分组和排序的工作。当到达 gap = 1 时,所有记录在统一组内排好序。
核心思想:先分组,然后每组进行直接插入排序。
gap 的取值方法有很多种,所以希尔排序的时间复杂度是不固定的。
而且希尔排序其实是对直接插入排序的优化,当 gap > 1 时所做的排序,其实就是预处理.
当 gap = 1 时,数据经历完预处理后是接近于有序的,所以这样就比直接使用插入排序要快。
根据上面的思路,我们就可以写代码了。
public static void shell(int[] arr) {
int gap = arr.length / 2;
while (gap > 1) {
shellSort(arr, gap);
gap /= 2;// 缩小增量
}
// gap 为 1 时还得再排序一遍
shellSort(arr, gap);
}
public static void shellSort(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (arr[j] > tmp) {
// 比 tmp 大就往后挪
arr[j + gap] = arr[j];
} else {
break;
}
}
arr[j + gap] = tmp;
}
}
写完后来测试一下。
没问题。
我们再来看看希尔排序的特点。
希尔排序总结:
时间复杂度:不固定
空间复杂度:O(1)
稳定性:不稳定
2.2 选择排序
2.2.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
2.2.2 直接选择排序
思路:用 i 遍历数组,假设 i 下标对应的值是最小的,然后从 i + 1下标遍历数组,如果还有比 i 更小的值,那最小的下标就更新,遍历完数组之后,再让 i 下标对应的值与 最小下标对应的值互换,那么 i 下标对应的值就有序了。
这个很简单,看思路就能直接写代码了。
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minIndex = i;
// 遍历找最小
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换
swap(arr, minIndex, i);
}
}
private static void swap(int[] arr, int minIndex, int i) {
int tmp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = tmp;
}
写完代码后测试下看看是否有误。
没问题。
还有第二种实现选择排序的方法:我们可以在一次遍历中同时寻找最大值下标和最小值下标,找完后就让最小值和数组最左边的值 arr[left] 交换,最大值和数组最右边的值 arr[right] 交换,然后再缩小边界(left++, right--),这样当 left 与 right 重合时,数组就有序了。
然后就可以根据上面的思路写出代码了,唯一要注意的就是,当 left 与 maxIndex 重合时,当 left 与 minIndex 交换完后,maxIndex 需要更新为 minIndex。
private static void swap(int[] arr, int j, int i) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
public static void selectSort2(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int minIndex = left;
int maxIndex = left;
// 在一次遍历中,寻找最大值和最小值的下标
for (int i = left + 1; i <= right; i++) {
if (arr[i] > arr[maxIndex]) {
maxIndex = i;
}
if (arr[i] < arr[minIndex]) {
minIndex = i;
}
}
// 找完后就交换
swap(arr, left, minIndex);
// 特殊情况,如果 max 刚好是 left
// 而刚刚又将 min 和 left 交换过了
// 所以此时的 max 应该是 min,所以需要更正 max
if (maxIndex == left) {
maxIndex = minIndex;
}
swap(arr, right, maxIndex);
left++;
right--;
}
}
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minIndex = i;
// 遍历找最小
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换
swap(arr, minIndex, i);
}
}
写完之后来测试下看看能否成功排序。
没有问题。
接下来我们看看直接选择排序的特点。
直接选择排序特点总结:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
2.2.3 堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
建好堆,顺序就排好了。
思路:如果是排升序,那么先建立一个大根堆(找到最后一颗子树的父亲节点,然后进行向下调整),然后让头节点和最后一个孩子节点进行交换,再进行向下调整即可。(删除堆顶元素的逻辑)。
public static void heapSort(int[] arr) {
createBigHeap(arr);
// 就跟删除差不多,删除头节点,把节点删的只剩下一个那就有序了
int end = arr.length - 1;
while (end != 0) {
// 交换头节点和最后一个孩子节点
swap(arr, 0, end);
// 交换完后向下调整
shiftDown(arr, 0, end);
end--;
}
}
private static void swap(int[] arr, int j, int i) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
private static void createBigHeap(int[] arr) {
// 找到最后一棵树的子树,然后进行向下调整即可。
for (int parent = (arr.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(arr, parent, arr.length);
}
}
private static void shiftDown(int[] arr, int parent, int end) {
// 先找到左右孩子的最大值,然后判断,交换,向下调整
int child = 2 * parent + 1;
while (child < end) {
if (child + 1 < end && arr[child + 1] > arr[child]) {
child++;
}
// 到这 child 一定是左右孩子的最大值
if (arr[parent] >= arr[child]) {
// 此时说明这棵树有序了
break;
} else {
// 交换
swap(arr, parent, child);
// 向下
parent = child;
child = 2 * parent + 1;
}
}
}
写完后我们来测试下看看。
没有问题。
那我们再来看看堆排序的特点。
堆排序特点总结:
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
2.3 交换排序
顾名思义,就是通过交换来排序,特点是,将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.1 冒泡排序
这个很简单,就是两层遍历。
如果是排升序,两两比较,如果较大的元素在前面,那就需要交换这两个元素的位置。
每次进行一趟排序,就能确定一个元素的最终位置。
根据以上过程图,我们可以总结一下:冒泡排序,有 n 个元素,就得进行 n - 1 趟排序,并且在每次排序中,j 的最大值小于等于元素总数 - 趟数,每次排序进行两两比较,当前元素大于后一个元素时,就要进行交换,而如果一整趟排序下来都没进行过交换的话,说明此时数组已经有序,可以退出循环了。
根据以上思路,我们就可以写代码了。
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
// 优化
boolean flg = false;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
flg = true;
swap(arr, j, j + 1);
}
}
if (!flg) {
break;
}
}
}
private static void swap(int[] arr, int j, int i) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
写完代码后我们可以来测试一下看看是否正确。
没问题。
那我们再来看看冒泡排序的特点。
冒泡排序特点总结:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
2.3.2 快速排序
快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
分为不同版本的原因是找基准的方法不同,思路都是一样的
找完基准后,递归基准左边和基准右边即可。
这个实现过程就跟二叉树前序遍历差不多,先把基准找到,然后递归基准左边,递归基准的右边即可。
1. Hoare 法
先假设 arr[left] 就是基准 key,让 right 往左找比 key 大的数,让 left 往右找比 key 小的数,然后交换这两个数,重复上述过程,直到 left 与 right 相遇才停止,然后交换 key 与 left 所对应的值,最后返回 left 就能得到基准了。
根据以上思路,我们就可以写代码啦。
public static void quickSort(int[] arr, int start, int end) {
// 如果左边是一个节点或者左边没有节点,那就可以不用递归了
if (start >= end) {
return;
}
// 找基准
int pivot = partition(arr, start, end);
// 递归基准左边
quickSort(arr, start, pivot - 1);
// 递归基准右边
quickSort(arr, pivot + 1, end);
}
private static int partition(int[] arr, int left, int right) {
int key = arr[left];
int index = left;
while (left < right) {
// right 往前找比 key 小的
while (left < right && arr[right] >= key) {
right--;
}
// left 往后找比 key 大的
while (left < right && arr[left] <= key) {
left++;
}
// 到了这里,arr[right] 一定小于 key
// arr[left] 一定大于 key
// 交换 arr[left] 和 arr[right]
swap(arr, left, right);
}
// 最后当 left 和 right 相遇时,交换
swap(arr, index, left);
return left;
}
private static void swap(int[] arr, int j, int i) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
接下来我们来测试下看看代码是否正确。
没问题,那我们来看看快速排序的特点。
还有一些小问题:
2. 挖坑法
跟上面的 Hoare 法差不多,也是先把 arr[left] 的值 tmp 存起来(相当于坑),让 right 往左走找比 tmp 小的数字,找到了就把这个数字(坑)赋给 arr[left] ,然后再让 left 往左走找比 tmp 大的数字,找到了就把这个数字放进坑里,重复这个过程,直到 left 和 right 相遇(坑),然后再把 tmp 放进坑里。
根据以上思路,我们就可以写代码啦。
// 挖坑法
private static int partition2(int[] arr, int left, int right) {
int tmp = arr[left];
while (left < right) {
// right 找比 tmp 小的数
while (left < right && arr[right] >= tmp) {
right--;
}
// 此时,arr[right] < tmp,填坑
arr[left] = arr[right];
// 此时,新的坑就是 right 下标
// left 找比 tmp 大的数
while (left < right && arr[left] <= tmp) {
left++;
}
// 此时,arr[left] > tmp,填坑
arr[right] = arr[left];
}
// left 和 right 相遇,填坑
arr[left] = tmp;
return left;
}
public static void quickSort(int[] arr, int start, int end) {
// 如果左边是一个节点或者左边没有节点,那就可以不用递归了
if (start >= end) {
return;
}
// 找基准
int pivot = partition2(arr, start, end);
// 递归基准左边
quickSort(arr, start, pivot - 1);
// 递归基准右边
quickSort(arr, pivot + 1, end);
}
写完之后测试下看看是否能成功排序。
没啥问题,我们再来看看最后一个找基准的方法。
3. 前后指针法
这个考试基本不考,会就行,考试考的最多的时挖坑法,其次是 Hoare 法。
根据以上思路,我们就可以写代码啦。
private static int partition3(int[] arr, int left, int right) {
int prev = left;
int cur = left + 1;
int key = arr[left];
while (cur <= right) {
// 如果 cur 找到了比 key 小的数,那么 prev 往后走,再判断 prev 和 cur 之间是否有元素(比 key 大)
if (arr[cur] < key && arr[++prev] != arr[cur]) {
swap(arr, prev, cur);
}
cur++;
}
swap(arr, prev, left);
return prev;
}
public static void quickSort(int[] arr, int start, int end) {
// 如果左边是一个节点或者左边没有节点,那就可以不用递归了
if (start >= end) {
return;
}
// 找基准
int pivot = partition3(arr, start, end);
// 递归基准左边
quickSort(arr, start, pivot - 1);
// 递归基准右边
quickSort(arr, pivot + 1, end);
}
private static void swap(int[] arr, int j, int i) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
写完之后我们可以来测试一下。
没有问题,接下来我们来看看快速排序该怎么优化。
2.3.3 快速排序优化
以上快速排序的代码都是没加优化的,所以会比较慢。
当数据量非常多的时候,会发生栈溢出。
所以我们需要优化快速排序,而常见的优化方法有以下两种:1. 三数取中。2. 递归到小的子区间时,可以使用插入排序的方法。
1. 三数取中法选 key
根据上面的思路,我们就可以写代码啦。
private static int midOfThree(int[] arr, int left, int right) {
// 防止溢出
int mid = left + (right - left) / 2;
int max = (arr[left] > arr[mid] ? arr[left] : arr[mid]);
max = max > arr[right] ? max : arr[right];
int min = (arr[left] < arr[mid] ? arr[left] : arr[mid]);
min = min < arr[right] ? min : arr[right];
if (arr[left] != min && arr[left] != max) {
return left;
} else if (arr[mid] != min && arr[mid] != max) {
return mid;
} else {
return right;
}
}
public static void quickSort2(int[] arr, int start, int end) {
// 如果左边是一个节点或者左边没有节点,那就可以不用递归了
if (start >= end) {
return;
}
// 三数取中
int index = midOfThree(arr, start, end);
// 交换
swap(arr, start, index);
// 找基准
int pivot = partition3(arr, start, end);
// 递归基准左边
quickSort2(arr, start, pivot - 1);
// 递归基准右边
quickSort2(arr, pivot + 1, end);
}
测试较小数据的话,排序是正常的。
数据量较大的话,还是会栈溢出,说明快排的优化还是不够,这时候 我们可以再加一个优化。
2. 小区间使用插入排序
递归到小的子区间时,可以考虑使用插入排序。
private static void insertSortRange(int[] arr, int start, int end) {
for (int i = start + 1; i <= end; i++) {
int tmp = arr[i];
int j = i - 1;
for (; j >= start; j--) {
if (arr[j] > tmp) {
arr[j + 1] = arr[j];
} else {
break;
}
}
arr[j + 1] = tmp;
}
}
public static void quickSort2(int[] arr, int start, int end) {
// 如果左边是一个节点或者左边没有节点,那就可以不用递归了
if (start >= end) {
return;
}
if (end - start + 1 <= 10) {
// 插入排序
insertSortRange(arr, start, end);
return;
}
// 三数取中
int index = midOfThree(arr, start, end);
// 交换
swap(arr, start, index);
// 找基准
int pivot = partition3(arr, start, end);
// 递归基准左边
quickSort2(arr, start, pivot - 1);
// 递归基准右边
quickSort2(arr, pivot + 1, end);
}
private static void swap(int[] arr, int j, int i) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
我们来测试下看看是否正确。
再来跟之前没加优化的快速排序对比下看看:
public class Test {
public static void main(String[] args) {
// int[] arr = {3, 2, 1, 5, 4, 4};
// Sort.insertSort(arr);
// System.out.println(Arrays.toString(arr));
// int[] arr2 = {4, 1, 3, 9, 2, 8, 7, 5, 6, 5};
// Sort.shell(arr2);
// System.out.println(Arrays.toString(arr2));
// int[] arr3 = {1, 5, 2, 5, 4};
// Sort.selectSort(arr3);
// System.out.println(Arrays.toString(arr3));
// int[] arr4 = {5, 1, 2, 5, 4};
// Sort.selectSort2(arr4);
// System.out.println(Arrays.toString(arr4));
// int[] arr5 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
// Sort.heapSort(arr5);
// System.out.println(Arrays.toString(arr5));
// int[] arr6 = {5, 1, 3, 7, 9, 8, 6, 2, 4, 5};
// Sort.bubbleSort(arr6);
// System.out.println(Arrays.toString(arr6));
// int[] arr7 = {6, 1, 2, 7, 9, 3, 4, 5, 6, 8};
// Sort.quickSort2(arr7, 0, arr7.length - 1);
// System.out.println(Arrays.toString(arr7));
//
int[] arr = new int[10_0000];
getRandomArr(arr);
testQuickSort1(arr);
testQuickSort2(arr);
}
public static void getRandomArr(int[] arr) {
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(100000);
}
}
public static void testQuickSort1(int[] arr) {
int[] tmpArr = Arrays.copyOf(arr, arr.length);
long start = System.currentTimeMillis();
Sort.quickSort(arr, 0, arr.length - 1);
long end = System.currentTimeMillis();
System.out.println("未加优化的快速排序耗时:" + (end - start));
}
public static void testQuickSort2(int[] arr) {
int[] tmpArr = Arrays.copyOf(arr, arr.length);
long start = System.currentTimeMillis();
Sort.quickSort2(arr, 0, arr.length - 1);
long end = System.currentTimeMillis();
System.out.println("加了优化的快速排序耗时:" + (end - start));
}
}
嗯嗯,很快呐。
2.3.4 快速排序非递归
我们可以借助栈来模拟递归实现,然后找基准,找到基准后,来判断基准左右两边的元素个数,如果基准左边元素个数大于等于两个,就将基准左边的 start 和 end 入栈,如果基准右边的元素大于等于两个(如果基准右边没有或只有一个元素,就说明这个基准右边的区间是有序的),就将基准右边的 start 和 end 入栈,然后判断栈是否为空,如果栈不为空,就弹出两个元素分别给 end 和 start,然后找基准,判断基准左右两边的元素个数(跟上面的一样),直到栈为空才停止。
根据以上思路,我们就可以写代码啦。
public static void quickSortNor(int[] arr) {
int start = 0;
int end = arr.length - 1;
Stack<Integer> stack = new Stack<>();
// 找基准
int pivot = partition2(arr, start, end);
if (pivot - start >= 2) {
// 基准左边有两个或以上元素,左边区间入栈
stack.push(start);
stack.push(pivot - 1);
}
if (end - pivot >= 2) {
// 基准右边有两个或以上元素,右边区间入栈
stack.push(pivot + 1);
stack.push(end);
}
while (!stack.isEmpty()) {
end = stack.pop();
start = stack.pop();
// 找基准
pivot = partition2(arr, start, end);
if (pivot - start >= 2) {
// 基准左边有两个或以上元素,左边区间入栈
stack.push(start);
stack.push(pivot - 1);
}
if (end - pivot >= 2) {
// 基准右边有两个或以上元素,右边区间入栈
stack.push(pivot + 1);
stack.push(end);
}
}
}
// 挖坑法
private static int partition2(int[] arr, int left, int right) {
int tmp = arr[left];
while (left < right) {
// right 找比 tmp 小的数
while (left < right && arr[right] >= tmp) {
right--;
}
// 此时,right 找到了比 tmp 小的数,填坑
arr[left] = arr[right];
//此时,新的坑就是 right 下标
// left 找比 tmp 大的数
while (left < right && arr[left] <= tmp) {
left++;
}
// 此时,left 找到了比 tmp 大的数,填坑
arr[right] = arr[left];
}
// left 和 right 相遇,填坑
arr[left] = tmp;
return left;
}
写完之后来测试看看效果。
没啥问题。
2.3.5 快速排序总结
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
2.4 归并排序
2.4.1 基本思想
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:先分解,再合并。
逻辑:先分解左边,再分解右边,最后进行合并(有序数组),跟二叉树前序遍历差不多。
当区间元素分解到只剩一个时,就不用再分解了,此时这个元素一定是有序的。
根据以上思路,我们就可以写代码啦。
public static void mergeSort(int[] arr, int left, int right) {
if (left >= right) {
// 如果区间只有一个元素
return;
}
// 均匀分割
int mid = left + (right - left) / 2;
// 分割左边
mergeSort(arr, left, mid);
// 分割右边
mergeSort(arr, mid + 1, right);
// 合并
merge(arr, left, mid, right);
}
// 合并有序数组
private static void merge(int[] arr, int left, int mid, int right) {
int[] tmpArr = new int[right - left + 1];
int i = left, j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tmpArr[k++] = arr[i++];
} else {
tmpArr[k++] = arr[j++];
}
}
while (i <= mid) {
tmpArr[k++] = arr[i++];
}
while (j <= right) {
tmpArr[k++] = arr[j++];
}
//拷贝回原数组
for (i = 0; i < tmpArr.length; i++) {
arr[i + left] = tmpArr[i];
}
}
写完之后可以来测试一下看看能否正常排序。
我们再来看看归并排序的特点。
我们再来看看归并排序的非递归实现方式。
public static void mergeSortNor(int[] arr) {
int gap = 1;// 分组
while (gap < arr.length) {
// 两组两组来合并
for (int i = 0; i < arr.length; i += 2 * gap) {
// 第一组的开始和结束
int left = i;
int mid = left + gap - 1;
// 第二组的结束
int right = mid + gap;
// 判断 mid 和 right 是否越界
if (mid >= arr.length) {
mid = arr.length - 1;
}
if (right >= arr.length) {
right = arr.length - 1;
}
// 合并
merge(arr, left, mid, right);
}
gap *= 2;
}
}
// 合并有序数组
private static void merge(int[] arr, int left, int mid, int right) {
int[] tmpArr = new int[right - left + 1];
int i = left, j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tmpArr[k++] = arr[i++];
} else {
tmpArr[k++] = arr[j++];
}
}
while (i <= mid) {
tmpArr[k++] = arr[i++];
}
while (j <= right) {
tmpArr[k++] = arr[j++];
}
//拷贝回原数组
for (i = 0; i < tmpArr.length; i++) {
arr[i + left] = tmpArr[i];
}
}
写完之后来测试一下看看能否正常排序。
没问题。
2.4.2 归并排序总结
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定
2.4.3 海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
3. 排序算法复杂度及稳定性分析
因为使用快速排序时一般都会加优化,所以快排时间复杂度是 O(N*logN) ,空间复杂度 O(logN)。
插入排序在数据完全有序情况下,时间复杂度为 O(N)。
冒泡排序在加了优化且数据完全有序时,时间复杂度为 O(N)。
快速排序在数据完全逆序情况下(单分支的树),时间复杂度为 O(N^2),空间复杂度 O(N)。
稳定的排序有:插入排序,冒泡排序,归并排序。
基于分治法实现的排序有:快速排序,归并排序。
4. 选择题
1. 快速排序算法是基于 ( ) 的一个排序算法。A :分治法 B :贪心法 C :递归法 D :动态规划法2. 对记录(54,38,96,23,15,72,60,45,83)进行从小到大的直接插入排序时,当把第 8 个记录 45 插入到有序表时,为找到插入位置需比较 ( ) 次?(采用从后往前比较)A: 3 B: 4 C: 5 D: 63. 以下排序方式中占用 O(n) 辅助存储空间的是 ( )A: 简单排序 B: 快速排序 C: 堆排序 D: 归并排序4. 下列排序算法中稳定且时间复杂度为 O(n^2) 的是 ( )A: 快速排序 B: 冒泡排序 C: 直接选择排序 D: 归并排序5. 关于排序,下面说法不正确的是 ( )A: 快排时间复杂度为 O(N*logN) ,空间复杂度为 O(logN)B: 归并排序是一种稳定的排序, 堆排序和快排均不稳定C: 序列基本有序时,快排退化成 " 冒泡排序 " ,直接插入排序最快D: 归并排序空间复杂度为 O(N), 堆排序空间复杂度的为 O(logN)6. 设一组初始记录关键字序列为 (65,56,72,99,86,25,34,66) ,则以第一个关键字 65 为基准而得到的一趟快速排序结果是( )A: 34 , 56 , 25 , 65 , 86 , 99 , 72 , 66 B: 25 , 34 , 56 , 65 , 99 , 86 , 72 , 66C: 34 , 56 , 25 , 65 , 66 , 99 , 86 , 72 D: 34 , 56 , 25 , 65 , 99 , 86 , 72 , 66
答案:ACDBDA
2. 要排 45,那么 45 之前一定是有序的,所以直接将 45 之前的数据排升序,再来让 45 比较即可
6. 用挖坑法找基准。