前言
这一篇文章主要想分享上次未讲完的两个排序算法——快速排序和归并排序(上一篇讲了前五个排序算法分别是,插入排序,希尔排序,选择排序,堆排序,冒泡排序,有兴趣可以去上一篇–>七大排序算法——详细介绍(上)
一、快速排序
1、快速排序基本思想
基本思想:
任取待排序列中某元素为基准值,然后以该基准值为中把该序列分成左右两组,左子列中所有元素均小于基准值,右子列中所有元素均大于基准值,然后左右序列重复此过程,知道所有元素有序(排列在相对应的位置)。
2、代码实现
(1)递归实现
// 快速排序
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
public static void quick(int[] array, int start, int end) {
if (start >= end) return;
int pivot = partition(array, start, end);
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}
public static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
(2)非递归实现
public void quickSortNor(int[] array) {
Stack<Integer> stack = new Stack<>();
int left = 0;
int right = array.length - 1;
int pivot = partition(array, left, right);
if (pivot - 1 > left) {
stack.push(left);
stack.push(pivot - 1);
}
if (pivot + 1 < right) {
stack.push(pivot + 1);
stack.push(right);
}
while (!stack.isEmpty()) {
right = stack.pop();
left = stack.pop();
pivot = partition(array, left, right);
if (pivot - 1 > left) {
stack.push(left);
stack.push(pivot - 1);
}
if (pivot + 1 < right) {
stack.push(pivot + 1);
stack.push(right);
}
}
}
partition
有两种实现方法,
Hoare方法
其步骤为:
- 选择基准元素:选择一个基准元素,通常是序列中第一个或者最后一个元素。
- 分区过程:使用两个指针,
(1)左指针向右移动,直到找到一个大于基准元素的元素
(2)右指针向左移动,直到找到一个小于基准元素的元素
(3)然后交换左右指针所指的元素,重复此过程直到左指针大于等于右指针。 - 交换基准元素:当退出上述的循环重复时,即满足条件左指针大于等于右指针时,交换基准元素与左指针或右指针所指向的元素->
swap(array,i,left)
。 - 递归排序:递归的对基准元素左右子序列进行排序,重复上述步骤。
public static int partition(int[] array, int left, int right) {
int i = left;
int key = array[left];
while (left < right) {
while (left < right && array[right] >= key) {
right--;
}
while (left < right && array[left] <= key) {
left++;
}
swap(array, left, right);
}
swap(array, i, left);
return left;
}
挖坑法(Lomuto):
其步骤为:
-
选择基本元素:同样的选择一个基准元素,通常是序列的第一个或最后一个元素。
-
挖坑过程:将基准元素挖出,形成一个坑,同样有左右两个指针,
(1)先是right
从序列的最右端向左走,直到找到小于基准元素的元素,然后将其填到基准元素的坑中,原来所在位置就代替为新的坑。
(2)再是left
向右走,直到找到大于基准元素的元素,然后将其填到上一个形成的坑中即right
所指向的位置,自身再代替其形成新的坑。
(3)循环重复此过程,直到左指针大于等于右指针 -
填入基本元素:把保存的基本元素,即
key
填入到最后形成的坑中,即array[left] = key
。 -
递归排序:递归的对基准元素左右子序列进行排序,重复上述步骤。
public int partitionHole(int[] array, int left, int right) {
int key = array[left];
while (left < right) {
while (left < right && array[right] >= key) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= key) {
left++;
}
array[right] = array[left];
}
array[left] = key;
return left;
}
特点
挖坑法(Lomutp)比Hoarte法更稳定,挖坑法不会改变元素的相对顺序,而Hoare法的交换次数比挖坑法多,可能导致元素相对顺序发生变化,从而导致算法的不稳定。另外,Hoare法在处理含有大量重复元素的数组时表现较好,因为它能够将相等的元素均匀地分布在两个分区中,减少了递归深度。在实际应用中,我们还是要根据具体需要来选择排序方法。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
二、归并排序
1、归并排序基本思想
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的有效排序算法,算法的核心是采用分治法。递归的将待排序列分成两个子序列,直到每个子序列只包含一个元素,然后将其按照顺序合并。
2、代码实现
- 分解:递归的将待排序列分成两个子序列,直到每个子序列只包含一个元素为止。
- 合并:将两个已排序的子序列合并到一个有序的数组中,合并过程中比较两个序列的首元素,将较小的元素填入新的数组中,然后其序列的指针往后走,直到两个子序列的所有元素全部填入新数组中。
public void mergeSort(int[] array) {
mergeSplit(array, 0, array.length - 1);
}
private void mergeSplit(int[] array, int left, int right) {
if (left >= right) return;
int mid = left + (right - left) / 2;
mergeSplit(array, left, mid);
mergeSplit(array, mid + 1, right);
merge(array, left, right, mid);
}
private void merge(int[] array, int left, int right, int mid) {
int s1 = left;
int s2 = mid + 1;
int i = 0;
int[] tmp = new int[right - left + 1];
while (s1 <= mid && s2 <= right) {
if (array[s1] <= array[s2]) {
tmp[i++] = array[s1++];
} else {
tmp[i++] = array[s2++];
}
}
while (s1 <= mid) {
tmp[i++] = array[s1++];
}
while (s2 <= right) {
tmp[i++] = array[s2++];
}
for (int j = 0; j < tmp.length; j++) {
array[j + left] = tmp[j];
}
}
特点
- 归并排序的小缺点是在合并的过程中要创建一个新的数组来储存合并后的结果,需要O(N)的空间复杂度,归并排序思考的更多是在磁盘中的外排序问题。
- 时间复杂度:O(N)。
- 空间复杂度:O(N*logN)
- 稳定性:稳定