[高频算法题]——排序(Java实现)
NC140:排序
链接: 排序.
1.插入排序
解题思路:
代码实现:
public static void insertSort(int[] arr) {
int bound = 1;
// [0, bound) 是已排序区间.
// [bound, length) 待排序区间.
for (; bound < arr.length; bound++) {
int v = arr[bound];
int cur = bound - 1;
for (; cur >= 0; cur--) {
if (arr[cur] > v) {
arr[cur + 1] = arr[cur];
} else {
break;
}
}
arr[cur + 1] = v;
}
}
时间复杂度 :O(N^2)
空间复杂度 :O(1)
稳定性:稳定
性质:
a)当前数组比较短,排序速度很快
b)当前数组相对有序,排序速度也很快
2.希尔排序
解题思路:
代码实现:
public static void shellSort(int[] arr) {
// 指定 gap 序列, len/2, len/4, len/8,...,1
int gap = arr.length / 2;
while (gap >= 1) {
_shellSort(arr, gap);
gap = gap / 2;
}
}
public static void _shellSort(int[] arr, int gap) {
// 进行分组插排. 分组依据就是 gap.
// gap 同时也表示分的组数.
// 同组的相邻元素, 下标差值就是 gap
// 下面的代码其实和插入排序是一样的. 尤其是把 gap 设为 1
int bound = gap;
for (; bound < arr.length; bound++) {
int v = arr[bound];
int cur = bound - gap;
for (; cur >= 0; cur -= gap) {
if (arr[cur] > v) {
// 进行搬运
arr[cur + gap] = arr[cur];
} else {
break;
}
}
arr[cur + gap] = v;
}
}
时间复杂度 :O(N^2) --> O(N^1.3)
空间复杂度 :O(1)
稳定性:不稳定
3.选择排序
解题思路:
代码实现:
public static void selectSort(int[] arr) {
// 创建一个变量 bound 表示已排序区间和待排序区间的边界.
// [0, bound) 已排序区间
// [bound, length) 待排序区间
int bound = 0;
for (; bound < arr.length; bound++) {
// 里层循环要进行打擂台的过程.
// 擂台的位置就是 bound 下标的位置
for (int cur = bound + 1; cur < arr.length; cur++) {
if (arr[cur] < arr[bound]) {
// 如果发现挑战者比擂主小, 就交换两个元素
swap(arr, cur, bound);
}
}
}
}
时间复杂度 :O(N^2)
空间复杂度 :O(1)
稳定性:不稳定
4.堆排序
解题思路:
代码实现:
public static void heapSort(int[] arr) {
// 1. 先进行建堆
createHeap(arr);
// 2. 循环进行交换堆顶元素和最后一个元素的过程, 并且删除该元素, 进行向下调整
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;
}
// 经过上面的 if 之后, child 就指向了左右子树中的较大值.
// 比较 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;
}
时间复杂度 :O(NlogN)
空间复杂度 :O(1)
稳定性:不稳定
5.冒泡排序
解题思路:
代码实现:
public static void bubbleSort(int[] arr) {
// [0, bound) 已排序区间
// [bound, length) 待排序区间
int bound = 0;
for (; bound < arr.length; bound++) {
boolean isSorted = true;
for (int cur = arr.length - 1; cur > bound; cur--) {
if (arr[cur] < arr[cur - 1]) {
// 不符合升序 , 交换
swap(arr, cur, cur - 1);
isSorted = false;
}
}
if(isSorted){
break;
}
}
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
时间复杂度 :O(N^2)
空间复杂度 :O(1)
稳定性:稳定
6.快速排序
解题思路:
代码实现:
public static void quickSort(int[] arr) {
// 使用一个辅助方法进行递归.
// 辅助方法多了两个参数, 用来表示针对数组上的哪个区间
// 进行整理
_quickSort(arr, 0, arr.length - 1);
}
// [left, right]
public static void _quickSort(int[] arr, int left, int right) {
if (left >= right) {
// 如果区间为空或者区间只有一个元素, 不必排序
return;
}
// 使用 partition 方法来进行刚才描述的整理过程
// index 就是 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 i = left;
int j = right;
while (i < j) {
// 先从左往右找到一个比基准值大的元素
while (i < j && arr[i] <= v) {
i++;
}
// 再从右往左找到一个比基准值小的元素
while (i < j && arr[j] >= v) {
j--;
}
swap(arr, i, j);
}
// 如果发现 i 和 j 重叠了, 此时就需要把当前基准值元素
// 和 i j 重叠位置进行交换
swap(arr, i, right);
return i;
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
时间复杂度 :
平均:O(NlogN)
最坏:O(N^2) (这个数组正好是逆序的时候)
空间复杂度 : 取决于递归的深度
平均:O(logN)
最坏:O(N)
稳定性:不稳定
7.归并排序
解题思路:
代码实现:
public static void mergeSort(int[] arr) {
// 创建一个新的方法辅助递归. 新方法中多了两个参数
// 表示是针对当前数组中的哪个部分进行排序
// 前闭后开区间
_mergeSort(arr, 0, arr.length);
}
// [left, right) 前闭后开区间
// right - left 区间中的元素个数
public static void _mergeSort(int[] arr, int left, int right) {
if (right - left <= 1) {
// 如果当前待排序的区间里只有 1 个元素或者没有元素
// 就直接返回, 不需要任何排序动作
return;
}
// 先把当前 [left, right) 区间一分为二
int mid = (left + right) / 2;
// 分成了两个区间
// [left, mid) [mid, right)
// 当左侧区间的 _mergeSort 执行完毕后,
// 就认为 [left, mid) 就已经是有序区间了
_mergeSort(arr, left, mid);
// 当右侧区间的 _mergeSort 执行完毕后,
// 就认为 [mid, right) 就已经是有序区间了
_mergeSort(arr, mid, right);
// 接下来把左右两个有序的数组, 合并到一起!!
merge(arr, left, mid, right);
}
// merge 方法本身功能是把两个有序数组合并成一个有序数组.
// 待合并的两个数组就分别是:
// [left, mid)
// [mid, right)
public static void merge(int[] arr, int left, int mid, int right) {
if (left >= right) {
return;
}
// 创建一个临时的数组, 用来存放合并结果.
// 我们是希望这个数组能存下合并后的结果 right - left
int[] tmp = new int[right - left];
// 当前要把新的元素放到 tmp 数组的哪个下标上
int tmpSize = 0;
int l = left;
int r = mid;
while (l < mid && r < right) {
// 归并排序是稳定排序!!
// 此处的条件不要写作 arr[l] < arr[r]
if (arr[l] <= arr[r]) {
// arr[l] 比较小, 就把这个元素先插入到 tmp 数组末尾
tmp[tmpSize] = arr[l];
tmpSize++;
l++;
} else {
// arr[r] 比较小, 就把这个元素插入到 tmp 数组的末尾
tmp[tmpSize] = arr[r];
tmpSize++;
r++;
}
}
// 当其中一个数组遍历完了之后, 就把另外一个数组的剩余部分都拷贝过来
while (l < mid) {
// 剩下的是左半边数组
tmp[tmpSize] = arr[l];
tmpSize++;
l++;
}
while (r < right) {
// 剩下的是右半边数组
tmp[tmpSize] = arr[r];
tmpSize++;
r++;
}
// 最后一步, 再把临时空间的内容都拷贝回参数数组中.
// 需要把 tmp 中的内容拷贝回 arr 的 [left, right) 这一段空间里
// [left, right) 这个空间很可能不是从 0 开始的额.
for (int i = 0; i < tmp.length; i++) {
arr[left + i] = tmp[i];
}
}
时间复杂度 :O(NlogN)
空间复杂度 :O(N)
稳定性:稳定
总结:
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(nlog(n))~O(n^2) | O(n^2) | O(1) | 不稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlog(n)) | O(nlog(n)) | O(nlog(n)) | O(1) | 不稳定 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlog(n)) | O(nlog(n)) | O(n^2) | O(log(n)~O(n)) | 不稳定 |
归并排序 | O(nlog(n)) | O(nlog(n)) | O(nlog(n)) | O(n) | 稳定 |