数据结构--排序(二)
1. 堆排序
1.1 堆排序原理
基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区
间的最大的数。
注意: 排升序要建大堆;排降序要建小堆。
1.2 堆排序实现
public static void heapSort(int[] arr) {
// 先建堆
createHeap(arr);
// 取堆顶元素与最后一个元素交换,并删除最后一个元素
// 再从 0 开始调整
int heapSize = arr.length;
for (int i = 0; i < arr.length; i++) {
swap(arr, 0, heapSize - 1);
// 删除最后一个元素
heapSize--;
// 从 0 开始调整
shiftDown(arr, heapSize, 0);
}
}
public static void shiftDown(int[] arr, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
// 先找出左右子树比较大的~
if (child + 1 < size && arr[child + 1] > arr[child]) {
child = child + 1;
}
// 再去比较 child 和 parent
if (arr[parent] < arr[child]) {
swap(arr, parent, child);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
public static void createHeap(int[] arr) {
for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(arr, arr.length, i);
}
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
注意:
1.堆排序是不稳定
2.时间复杂度:O(n * log(n))
3.空间复杂度:O(1)
2. 冒泡排序
2.1 冒泡排序原理
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
2.2 冒泡排序实现
// 冒泡排序
public static void bubbleSort(int[] arr) {
for (int bound = 0; bound < arr.length; bound++) {
for (int cur = arr.length - 1; cur > bound; cur--) {
if (arr[cur - 1] > arr[cur]) {
swap(arr,cur - 1,cur);
}
}
}
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
注意:
1.冒泡排序是稳定的
2.时间复杂度:O(n^2)
3.空间复杂度:O(1)
3. 快速排序
3.1 快速排序原理
核心操作:partition
现在待排序数组中选取一个 “基准值” ,然后把这个数组整理成,左侧壁基准值小,右侧比基准值大(使用左右下标从两边往中间走实现)
3.2 快速排序实现(递归)
public static void quickSort(int[] arr) {
// 创建一个辅助递归的方法.
// 在这个方法的参数中, 明确指定针对哪个区间进行递归.
// [0, length - 1]
_quickSort(arr, 0, arr.length - 1);
}
public static void _quickSort(int[] arr, int left, int right) {
if (left >= right) {
// 如果当前的区间为空, 或者只有一个元素
// 都不需要进行任何处理
return;
}
// 现针对当前 [left, right] 区间进行 partition 操作
// 方法的返回值, 表示整理完当前区间后, 基准值所在的位置.
// 遍历过程中的 left 和 right 的重合位置
int index = partition(arr, left, right);
// 递归的对左侧区间进行快速排序
_quickSort(arr, left, index - 1);
// 递归的对右侧区间进行快速排序
_quickSort(arr, index + 1, right);
}
public static int partition(int[] arr, int left, int right) {
// 选取最右侧元素作为基准值.
int v = arr[right];
int l = left;
int r = right;
// 如果 l 和 r 重合, 说明遍历完成
while (l < r) {
// 先从左往右, 找一个比基准值大的数字.
while (l < r && arr[l] <= v) {
l++;
}
// 当循环结束的时候, l 就指向了比基准值大的元素
// 再从右往左, 找一个比基准值小的数字
while (l < r && arr[r] >= v) {
r--;
}
swap(arr, l, r);
}
// 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
swap(arr, l, right);
// 最终方法返回基准值所在的位置(l 和 r 重合的位置)
return l;
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
注意:
1.快速排序是不稳定的
2.时间复杂度:O(n * log(n))
3.空间复杂度:O(log(n))
快速排序的优化:
1.三数取中
2.当待处理区间比较小的时候,就不继续递归了,直接针对该区域进行插入排序;
3.当递归达到一定的深度,并且当前的待处理区间比较大,还可以使用堆排序。
3.3 快速排序实现(非递归)
public static void quickSortByLoop(int[] arr) {
// 1. 先创建一个栈, 这个栈用来保存当前的每一个待处理区间
Stack<Integer> stack = new Stack<>();
// 2. 把根节点入栈, 整个数组对应的区间
stack.push(0);
stack.push(arr.length - 1);
// 3. 循环取栈顶元素
while (!stack.isEmpty()) {
// 取的元素就是当前的待处理区间
// 取的顺序正好和插入的顺序相反
int right = stack.pop();
int left = stack.pop();
if (left >= right) {
// 如果是空区间或者只有一个元素, 不需要排序
continue;
}
// 调用 partition 方法整理当前区间
int index = partition(arr, left, right);
// 右侧区间: [index + 1, right]
stack.push(index + 1);
stack.push(right);
// 左侧区间: [left, index - 1]
stack.push(left);
stack.push(index - 1);
}
}
public static int partition(int[] arr, int left, int right) {
// 选取最右侧元素作为基准值.
int v = arr[right];
int l = left;
int r = right;
// 如果 l 和 r 重合, 说明遍历完成
while (l < r) {
// 先从左往右, 找一个比基准值大的数字.
while (l < r && arr[l] <= v) {
l++;
}
// 当循环结束的时候, l 就指向了比基准值大的元素
// 再从右往左, 找一个比基准值小的数字
while (l < r && arr[r] >= v) {
r--;
}
swap(arr, l, r);
}
// 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
swap(arr, l, right);
// 最终方法返回基准值所在的位置(l 和 r 重合的位置)
return l;
}
4. 归并排序
4.1 归并排序原理
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
4.2 归并排序实现(递归)
public static void mergeSort(int[] arr) {
_mergeSort(arr, 0, arr.length);
}
// 辅助递归的方法
public static void _mergeSort(int[] arr, int left, int right) {
if (right - left <= 1) {
// 判定当前区间是不是只有一个元素或者没有元素
// 此时不需要进行排序
return;
}
int mid = (left + right) / 2;
// 先让 [left, mid) 区间变成有序
_mergeSort(arr, left, mid);
// 再让 [mid, right) 区间变成有序
_mergeSort(arr, mid, right);
// 合并两个有序区间
merge(arr, left, mid, right);
}
// 归并排序中的关键操作, 就是归并两个有序数组.
// 使用该 merge 方法完成数组归并的过程
// 此处两个数组就通过参数的 left, mid, right 描述
// [left, mid) 左侧数组
// [mid, right) 右侧数组
public static void merge(int[] arr, int left, int mid, int right) {
// 进行具体的归并操作
// 需要创建一个临时的空间用来保存归并的结果
// 临时空间得能保存下带归并的两个数组.
// right - left 这么长
if (left >= right) {
// 空区间就直接忽略~~
return;
}
int[] tmp = new int[right - left];
int tmpIndex = 0; // 这个下标表示当前元素该放到临时空间的哪个位置上.
int cur1 = left;
int cur2 = mid;
while (cur1 < mid && cur2 < right) {
// 此处 最好写成 <= , 目的就是稳定性.
// 由于 cur1 是在左侧区间, cur2 是在右侧区间.
// 此时如果发现 cur1 和 cur2 的值相等,
// 就希望左侧区间的 cur1 在最终结果中仍然是在左侧.
// 于是就先把 cur1 对应的元素给先放到结果中.
if (arr[cur1] <= arr[cur2]) {
// 把 cur1 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
} else {
// 把 cur2 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
}
// 循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
while (cur1 < mid) {
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
}
while (cur2 < right) {
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
// 还需要把 tmp 的结果再放回 arr 数组. (原地排序)
// 把原始数组的 [left, right) 区间替换回排序后的结果
for (int i = 0; i < tmp.length; i++) {
arr[left + i] = tmp[i];
}
}
注意:
1.归并排序是稳定的
2.时间复杂度:O(n * log(n))
3.空间复杂度:O(n)
4.3 归并排序(非递归)
public static void mergeSortByLoop(int[] arr) {
// gap 用于限定分组.
// gap 值的含义就是每个待归并数组的长度
int gap = 1;
for (; gap < arr.length; gap *= 2) {
// 当前两个待归并的数组
for (int i = 0; i < arr.length; i += 2*gap) {
// 在这个循环中控制两个相邻的数组进行归并
// [left, mid) 和 [mid, right) 就要进行归并
int left = i;
int mid = i + gap;
if (mid >= arr.length) {
mid = arr.length;
}
int right = i + 2 * gap;
if (right >= arr.length) {
right = arr.length;
}
merge(arr, left, mid, right);
}
}
}
// 归并排序中的关键操作, 就是归并两个有序数组.
// 使用该 merge 方法完成数组归并的过程
// 此处两个数组就通过参数的 left, mid, right 描述
// [left, mid) 左侧数组
// [mid, right) 右侧数组
public static void merge(int[] arr, int left, int mid, int right) {
// 进行具体的归并操作
// 需要创建一个临时的空间用来保存归并的结果
// 临时空间得能保存下带归并的两个数组.
// right - left 这么长
if (left >= right) {
// 空区间就直接忽略~~
return;
}
int[] tmp = new int[right - left];
int tmpIndex = 0; // 这个下标表示当前元素该放到临时空间的哪个位置上.
int cur1 = left;
int cur2 = mid;
while (cur1 < mid && cur2 < right) {
// 此处 最好写成 <= , 目的就是稳定性.
// 由于 cur1 是在左侧区间, cur2 是在右侧区间.
// 此时如果发现 cur1 和 cur2 的值相等,
// 就希望左侧区间的 cur1 在最终结果中仍然是在左侧.
// 于是就先把 cur1 对应的元素给先放到结果中.
if (arr[cur1] <= arr[cur2]) {
// 把 cur1 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
} else {
// 把 cur2 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
}
// 循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
while (cur1 < mid) {
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
}
while (cur2 < right) {
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
// 还需要把 tmp 的结果再放回 arr 数组. (原地排序)
// 把原始数组的 [left, right) 区间替换回排序后的结果
for (int i = 0; i < tmp.length; i++) {
arr[left + i] = tmp[i];
}
}