文章目录
一、时间复杂度
二、空间复杂度
三、排序算法
1.插入排序(熟悉)
public class Solution {
// 插入排序:稳定排序,在接近有序的情况下,表现优异
public int[] sortArray(int[] nums) {
int len = nums.length;
// 循环不变量:将 nums[i] 插入到区间 [0, i) 使之成为有序数组
for (int i = 1; i < len; i++) {
// 先暂存这个元素,然后之前元素逐个后移,留出空位
int temp = nums[i];
int j = i;
// 注意边界 j > 0
while (j > 0 && nums[j - 1] > temp) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
return nums;
}
}
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
- 最好情况下,即待排序序列已按关键字有序,每趟操作只需 1 次比较 0 次移动。
- 最坏情况下,即待排序序列按关键字逆序排序,这时在第 j 趟操作中,为插入元素需要同前面的 j 个元素进行 j 次关键字比较,移动元素的次数为 j+1 次。
- 平均情况下:即在第 j 趟操作中,插入记录大约需要同前面的 j/2 个元素进行关键字比较,移动记录的次数为 j/2+1 次。
2.希尔排序(不建议多花时间了解)
3.冒泡排序(了解)
public class Solution {
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];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
return nums;
}
}
- 假设待排序的元素个数为 n,则总共要进行 n-1 趟排序,对 j 个元素的子序列进行一趟起泡排序需要进行 j-1 次关键字比较。
4.快速排序(重要)
- 最坏情况是每次进行划分时,在所得到的两个子序列中有一个子序列为空。
- 最好情况是在每次划分时,都将序列一分为二,正好在序列中间将序列分成长度相等的两个子序列。
class Solution {
//利用快速排序解决
public int[] sortArray(int[] nums) {
//定义一个左指针,指向数组元素的第一个元素
int left = 0;
//定义一个右指针,指向数组元素的最后一个
int right = nums.length-1;
//定义一个快速排序的方法
return quickSort(nums,left,right);
}
public int[] quickSort(int[] nums, int left,int right){
//如果左指针大于右指针,怎退出循环
if(left > right){
return null;
}
//定一个基数,指向数组的最左边的元素
int base = nums[left];
//定义一个左指针,指向数组元素的第一个元素
int i = left;
//定义一个右指针,指向数组元素的最后一个
int j = right;
//当左右指针不相等时,就继续移动左右指针
while(i != j){
//从右往左遍历,当右指针指向的元素大于等于基数时,j--。右指针持续向左移动
while(nums[j]>=base && i < j){
j--;
}
//从左往右遍历,当左指针指向的元素小于等于基数时,i++。左指针持续向右移动
while(nums[i]<=base && i < j){
i++;
}
//当左右两个指针停下来时,交换两个元素
swap(nums, i, j);
}
//当左右指针相遇时,将左右指针同时指向的元素和基数进行交换。
swap(nums,i,left);//这个看着可能会变扭,等同于小面两行代码。
//不过这下面的两行的代码的顺序不能相反,否则导致结果,都为第一个基数。
//首先把基数要填入的位置空出来,然后在将基数填入。
// nums[left] = nums[i];
// nums[i] = base;
quickSort(nums,left, i-1);
quickSort(nums,i+1,right);
return nums;
}
public void swap(int[] nums,int left, int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
5.选择排序(了解)
import java.util.Arrays;
public class Solution {
// 选择排序:每一轮选择最小元素交换到未排定部分的开头
public int[] sortArray(int[] nums) {
int len = nums.length;
// 循环不变量:[0, i) 有序,且该区间里所有元素就是最终排定的样子
for (int i = 0; i < len - 1; i++) {
// 选择区间 [i, len - 1] 里最小的元素的索引,交换到下标 i
int minIndex = i;
for (int j = i + 1; j < len; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
swap(nums, i, minIndex);
}
return nums;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
public static void main(String[] args) {
int[] nums = {5, 2, 3, 1};
Solution solution = new Solution();
int[] res = solution.sortArray(nums);
System.out.println(Arrays.toString(res));
}
}
- 在简单选择排序中,所需移动元素的次数较少,在待排序序列已经有序的情况下,简单选择排序不需要移动元素,在最坏的情况下,即待排序序列本身是逆序时,则移动元素的次数为 3(n-1)。然而无论简单选择排序过程中移动元素的次数是多少,在任何情况下,简单选择排序都需要进行n(n-1)/2 次比较操作,因此简单选择排序的时间复杂度为Ο(n2)。
6.堆排序(堆很重要,堆排序根据个人情况掌握)
class Solution{
public int[] sortArray(int[] nums) {
heapSort(nums);
return nums;
}
/**
* 堆排序(宇宙无敌的JAVA)
* 第一步: 构建大顶堆
* 第二步: 交换堆点元素(堆点元素与当前处理的二叉树最后一个元素交换)
* 第三步: 去除二叉树最后一个节点, 对二叉树根节点堆化(heapify)
* 第四步: 重复第二、第三步直至结束
*/
private void heapSort(int[] nums) {
int len = nums.length;
//第一步: 构建大顶堆
buildMaxHead(nums, len);
//第四步: 重复第二、第三步直至结束
for (int i = len - 1; i >= 1; i--) {
//第二步: 交换堆点元素(堆点元素与当前处理的二叉树最后一个元素交换)
swap(nums, i, 0);
//第三步: 去除二叉树最后一个节点, 对二叉树根节点堆化(heapify)
// 元素少于两个没有必要再处理, 这里不特殊判断处理
heapify(nums, i, 0);
}
}
/**
* 构建大顶堆
*/
private void buildMaxHead(int[] nums, int len) {
//从倒数第二层数的节点开始, 一直到二叉树根节点, 进行堆化(heapify)
// 求取最后一个节点的父节点, 父节点索引为 int parentIndex = (i - 1) / 2
int lastNodeIndex = ((len - 1) - 1) / 2;
for (int i = lastNodeIndex; i >= 0; i--) {
heapify(nums, len, i);
}
}
private void heapify(int[] nums, int len, int cur) {
if (cur >= len){
return;
}
//第一个子节点的索引位置 int c1 = 2 * i + 1
//第二个子节点的索引位置 int c2 = 2 * i + 2
int c1 = 2 * cur + 1;
int c2 = 2 * cur + 2;
//求最大值的索引
int maxIndex = cur;
if (c1 < len && nums[c1] > nums[maxIndex]){
maxIndex = c1;
}
if (c2 < len && nums[c2] > nums[maxIndex]){
maxIndex = c2;
}
//当前节点cur不是该堆的最大值, 交换元素值, 并递归被交换点进行堆化(heapify)
if (cur != maxIndex){
swap(nums, cur, maxIndex);
heapify(nums, len, maxIndex);
}
}
/**
* 交换元素值
*/
private void swap(int[] nums, int x, int y) {
int temp = nums[x];
nums[x] = nums[y];
nums[y] = temp;
}
}
7.归并排序(重点)
public class Solution {
/**
* 列表大小等于或小于该大小,将优先于 mergeSort 使用插入排序
*/
private static final int INSERTION_SORT_THRESHOLD = 7;
public int[] sortArray(int[] nums) {
int len = nums.length;
int[] temp = new int[len];
mergeSort(nums, 0, len - 1, temp);
return nums;
}
/**
* 对数组 nums 的子区间 [left, right] 进行归并排序
* @param temp 用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁
*/
private void mergeSort(int[] nums, int left, int right, int[] temp) {
// 小区间使用插入排序
if (right - left <= INSERTION_SORT_THRESHOLD) {
insertionSort(nums, left, right);
return;
}
int mid = left + (right - left) / 2;
// Java 里有更优的写法,在 left 和 right 都是大整数时,即使溢出,结论依然正确
// int mid = (left + right) >>> 1;
mergeSort(nums, left, mid, temp);
mergeSort(nums, mid + 1, right, temp);
// 如果数组的这个子区间本身有序,无需合并
if (nums[mid] <= nums[mid + 1]) {
return;
}
mergeOfTwoSortedArray(nums, left, mid, right, temp);
}
/**
* 对数组 arr 的子区间 [left, right] 使用插入排序
*/
private void insertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int temp = arr[i];
int j = i;
while (j > left && arr[j - 1] > temp) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp;
}
}
/**
* 合并两个有序数组:先把值复制到临时数组,再合并回去
* @param mid [left, mid] 有序,[mid + 1, right] 有序
* @param temp 全局使用的临时数组
*/
private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
System.arraycopy(nums, left, temp, left, right + 1 - left);
int i = left;
int j = mid + 1;
for (int k = left; k <= right; k++) {
if (i == mid + 1) {
nums[k] = temp[j];
j++;
} else if (j == right + 1) {
nums[k] = temp[i];
i++;
} else if (temp[i] <= temp[j]) {
// 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
nums[k] = temp[i];
i++;
} else {
// temp[i] > temp[j]
nums[k] = temp[j];
j++;
}
}
}
}
8.计数排序(了解)
public class Solution {
private static final int OFFSET = 50000;
public int[] sortArray(int[] nums) {
int len = nums.length;
// 由于 -50000 <= A[i] <= 50000
// 因此"桶" 的大小为 50000 - (-50000) = 10_0000
// 并且设置偏移 OFFSET = 50000,目的是让每一个数都能够大于等于 0
// 这样就可以作为 count 数组的下标,查询这个数的计数
int size = 10_0000;
// 计数数组
int[] count = new int[size];
// 计算计数数组
for (int num : nums) {
count[num + OFFSET]++;
}
// 把 count 数组变成前缀和数组
for (int i = 1; i < size; i++) {
count[i] += count[i - 1];
}
// 先把原始数组赋值到一个临时数组里,然后回写数据
int[] temp = new int[len];
System.arraycopy(nums, 0, temp, 0, len);
// 为了保证稳定性,从后向前赋值
for (int i = len - 1; i >= 0; i--) {
int index = count[temp[i] + OFFSET] - 1;
nums[index] = temp[i];
count[temp[i] + OFFSET]--;
}
return nums;
}
}
9.基数排序(了解)
public class Solution {
private static final int OFFSET = 50000;
public int[] sortArray(int[] nums) {
int len = nums.length;
// 预处理,让所有的数都大于等于 0,这样才可以使用基数排序
for (int i = 0; i < len; i++) {
nums[i] += OFFSET;
}
// 第 1 步:找出最大的数字
int max = nums[0];
for (int num : nums) {
if (num > max) {
max = num;
}
}
// 第 2 步:计算出最大的数字有几位,这个数值决定了我们要将整个数组看几遍
int maxLen = getMaxLen(max);
// 计数排序需要使用的计数数组和临时数组
int[] count = new int[10];
int[] temp = new int[len];
// 表征关键字的量:除数
// 1 表示按照个位关键字排序
// 10 表示按照十位关键字排序
// 100 表示按照百位关键字排序
// 1000 表示按照千位关键字排序
int divisor = 1;
// 有几位数,外层循环就得执行几次
for (int i = 0; i < maxLen; i++) {
// 每一步都使用计数排序,保证排序结果是稳定的
// 这一步需要额外空间保存结果集,因此把结果保存在 temp 中
countingSort(nums, temp, divisor, len, count);
// 交换 nums 和 temp 的引用,下一轮还是按照 nums 做计数排序
int[] t = nums;
nums = temp;
temp = t;
// divisor 自增,表示采用低位优先的基数排序
divisor *= 10;
}
int[] res = new int[len];
for (int i = 0; i < len; i++) {
res[i] = nums[i] - OFFSET;
}
return res;
}
private void countingSort(int[] nums, int[] res, int divisor, int len, int[] count) {
// 1、计算计数数组
for (int i = 0; i < len; i++) {
// 计算数位上的数是几,先取个位,再十位、百位
int remainder = (nums[i] / divisor) % 10;
count[remainder]++;
}
// 2、变成前缀和数组
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 3、从后向前赋值
for (int i = len - 1; i >= 0; i--) {
int remainder = (nums[i] / divisor) % 10;
int index = count[remainder] - 1;
res[index] = nums[i];
count[remainder]--;
}
// 4、count 数组需要设置为 0 ,以免干扰下一次排序使用
for (int i = 0; i < 10; i++) {
count[i] = 0;
}
}
/**
* 获取一个整数的最大位数
*
* @param num
* @return
*/
private int getMaxLen(int num) {
int maxLen = 0;
while (num > 0) {
num /= 10;
maxLen++;
}
return maxLen;
}
}
10.桶排序(了解)
基本思路:一个坑一个萝卜,也可以一个坑多个萝卜,对每个坑排序,再拿出来,整体就有序。