「这是我参与2022首次更文挑战的第31天,活动详情查看:2022首次更文挑战」
前言
笔者除了大学时期选修过《算法设计与分析》和《数据结构》还是浑浑噩噩度过的(当时觉得和编程没多大关系),其他时间对算法接触也比较少,但是随着开发时间变长对一些底层代码/处理机制有所接触越发觉得算法的重要性,所以决定开始系统的学习(主要是刷力扣上的题目)和整理,也希望还没开始学习的人尽早开始。
系列文章收录《算法》专栏中。
问题描述
给你一个整数数组 nums,请你将该数组升序排列。
示例 1:
输入: nums = [5,2,3,1]
输出: [1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
提示:
- 1 <= nums.length <= 5 * 104
- -5 * 104 <= nums[i] <= 5 * 104
剖析
对数组排序大家都接触过,所以下面不进行详细剖析,可以通算法可视化网站进行感知。
冒泡排序
核心:利用比较,每次都把最大的沉在底部。比较好理解,但效率太低一般不用。时间复杂度为O(n^2) 空间复杂度为O(1)
/**
* 冒泡排序,每次遍历需要和其他数字进行比较,把最大的沉在最底部,已经沉在最底部的不用再参与比较
* 时间复杂度为O(n^2) 空间复杂度为O(1)
*/
public class BubbleSort {
public int[] sortArray(int[] nums) {
int len = nums.length;
for (int i = len - 1; i >= 0; i--) {
//如果没有进行交换就说明已经是有序的
boolean sorted = true;
for (int j = 0; j < i; j++) {
//小的下标比大的下标大就交换位置,这样大的数字才会在沉于底部
if (nums[j] > nums[j + 1]) {
int temp = nums[j + 1];
nums[j + 1] = nums[j];
nums[j] = temp;
sorted = false;
}
}
if (sorted) {
break;
}
}
return nums;
}
}
快速排序
核心:分而治之,最终递归到最内部,最内部有顺序了,整体就有顺序了。时间复杂度为期望O(nlogn),空间复杂度为O(logn)。
最简单的就是[3,2,1]取中心为中心轴,小于2的放左边,大于等于2的放右边,一下就进行了顺序排序。
最坏情况需要进行进行需要进行O(N^2)次比较,比如[1,2,3,...,n]并且以最左边或者最右边作为中心轴。所以中心轴最好不要选择两边的数字。
我们可以随机选取靠中间部分的数字作为中心轴,然后和最小下标进行调换,这样个人觉得比较好处理点(样每次都固定了中心轴的下标不用判断是否越过了中心轴),大于等于中心轴的从最右边开始放,小于等于中心轴的从最左边开始放,从右边指针开始进行比较,如果大于等于当前右边指针指向的元素就不调换继续移动右指针,小于了就和右边指针指向的元素就行调换,右边指针开始移动进行比较,同样如果小于就不调换继续移动,调换发生了指针就交替移动。选取和移动总结下如下:
- 随机选取靠中间部分的数字作为中心轴,然后和最小下标进行交换,方便处理。
- 因为中心轴是和最小下标进行调换,所以从最右边的指针开始进行和中心轴的比较大小。
- 左右指针的移动交替发生在调换时,不发生时一直移动直到左右指针相碰结束。
上面的步骤已经把分好了左右两块,后面就是对左右两块进行继续相同动作,这就是分而治之。
import java.util.Random;
public class QuickSort {
public int[] sortArray(int[] nums) {
randomizedQuicksort(nums, 0, nums.length - 1);
return nums;
}
public void randomizedQuicksort(int[] nums, int l, int r) {
if (l < r) {
//随机下标和最左边下标元素进行调换
int random = new Random().nextInt(r - l + 1) + l;
int randomTemp = nums[random];
nums[random] = nums[l];
nums[l] = randomTemp;
//选取最左下标作为中心轴pivot
int pivot = nums[l];
int lTemp = l;
//先移动右指针
for (int i = r; i > lTemp; i--) {
//右边的小于了就要丢到左边了
if (nums[i] < pivot) {
nums[lTemp] = nums[i];
lTemp++;
//左边的小于了就要丢到右边了
for (int j = lTemp; j < i; j++) {
if (nums[j] >= pivot) {
nums[i] = nums[j];
break;
}
lTemp++;
}
}
}
//碰撞的位置就是中心轴的最终位置
nums[lTemp] = pivot;
randomizedQuicksort(nums, l, lTemp - 1);
randomizedQuicksort(nums, lTemp + 1, r);
}
}
public static void main(String[] args) {
QuickSort quickSort = new QuickSort();
System.out.println(quickSort.sortArray(new int[]{1, 5, 7, 3, 2}));
}
}
堆排序
核心:就是从下往上使根(下标为0)成为最大值,然后0下标和最大小标交换,数组长度减小1,这样最大值就不会继续参与大根堆的构造,然后继续使重新构造的堆的根成为最大值,以此类推,完成排序。时间复杂度为O(nlogn),空间复杂度为O(1)。
package com.study.algorithm.sort;
public class HeapSort {
public int[] sortArray(int[] nums) {
heapSort(nums);
return nums;
}
public void heapSort(int[] nums) {
int len = nums.length - 1;
//从底部开始构建大根堆
buildMaxHeap(nums, len);
for (int i = len; i >= 1; --i) {
//每次0下标都是最大的,i从最大下标开始逐渐递减和0下标进行交换
swap(nums, i, 0);
len -= 1;
//因为最开始是从底部开始构建的并且和父节点调换后也会继续往下构建达到局部大根堆,就算0被替换了下面的两个节点都比他们各种的子节点大
maxHeapify(nums, 0, len);
}
}
public void buildMaxHeap(int[] nums, int len) {
//length整除2能定位到倒数第二层的最右边的节点
for (int i = len / 2; i >= 0; --i) {
maxHeapify(nums, i, len);
}
}
public void maxHeapify(int[] nums, int i, int len) {
//根据最右边的节点取到两个子节点
for (; (i << 1) + 1 <= len; ) {
int lson = (i << 1) + 1;
int rson = (i << 1) + 2;
int large;
if (lson <= len && nums[lson] > nums[i]) {
large = lson;
} else {
large = i;
}
if (rson <= len && nums[rson] > nums[large]) {
large = rson;
}
//进行交换
if (large != i) {
swap(nums, i, large);
//交换完之后会看是否还有子节点,有的话继续调换使变成局部大根堆
i = large;
} else {
break;
}
}
}
/**
* 交换
*
* @param nums
* @param i
* @param j
*/
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public static void main(String[] args) {
HeapSort heapSort = new HeapSort();
System.out.println(heapSort.sortArray(new int[]{4, 6, 8, 5, 9}));
}
}