算法 | 时间复杂度 | 空间复杂度 | 稳定性 | |
选择 | O(N2) | O(1) | 不稳定 | |
冒泡 | O(N2) | O(1) | 稳定 | |
插入 | O(N2) | O(1) | 稳定 | |
归并 | O(N*logN) | O(N) | 稳定 | |
堆排序 | O(N*logN) | O(1) | 不稳定 | |
快速排序 | O(N*logN) | O(logN) | 不稳定 | |
桶排序 | O(N) | O(N) | 稳定 | |
辅助记忆
时间复杂度记忆-
冒泡、选择、直接 排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n2)O(n2)(一遍找元素O(n),一遍找位置O(n))
快速、归并、希尔、堆基于二分思想,log以2为底,平均时间复杂度为O(nlogn)(一遍找元素O(n),一遍找位置O(logn)
稳定性记忆-“快希选堆”(快牺牲稳定性)
排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定
快速排序(重点)
基本思路:快速排序每一次都排定一个元素(这个元素呆在了它最终应该呆的位置),然后递归地去排它左边的部分和右边的部分,依次进行下去,直到数组有序;
算法思想:分而治之(分治思想),与「归并排序」不同,「快速排序」在「分」这件事情上不想「归并排序」无脑地一分为二,而是采用了 partition 的方法(书上,和网上都有介绍,就不展开了),因此就没有「合」的过程。
实现细节(注意事项):(针对特殊测试用例:顺序数组或者逆序数组)一定要随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」);
//双指针对撞partition
public int[] sortArray(int[] nums) {
int len=nums.length;
if(len==0){
return new int[0];
}
quickSort(nums,0,len-1);
return nums;
}
public void quickSort(int[] nums,int left,int right){
if (left < right) {
//随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的
//时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」)
swap(nums, left + (int) (Math.random() * (right - left + 1)), left);
int index = partition(nums,left, right);//随机切分点
quickSort(nums, left, index - 1); // 排序< 区
quickSort(nums, index + 1, right); // 排序> 区
}
}
public int partition(int[]nums,int left,int right){
int pivot=nums[left];
int i=left+1;
int j=right;
while(true){
while(i<=j&&nums[i]<pivot){
i++;
}
while(i<=j&&nums[j]>pivot){
j--;
}
if(i>j){
break;
}
swap(nums,i,j);
i++;
j--;
}
swap(nums,left,j);
return j;
}
public void swap(int[] nums,int index1,int index2){
int temp=nums[index1];
nums[index1]=nums[index2];
nums[index2]=temp;
}
复杂度分析:
时间复杂度:O(NlogN),这里 N 是数组的长度;
空间复杂度:O(logN),这里占用的空间主要来自递归函数的栈空间
tips:快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后续遍历
快速排序的逻辑是,若要对nums[lo..hi]进行排序,我们先找一个分界点p,通过交换元素使得nums[lo..p-1]都小于等于nums[p],且nums[p+1..hi]都大于nums[p],然后递归地去nums[lo..p-1]和nums[p+1..hi]中寻找新的分界点,最后整个数组就被排序了。
快速排序的代码框架如下:
void sort(int[] nums, int lo, int hi) {
/****** 前序遍历位置 ******/
// 通过交换元素构建分界点 p
int p = partition(nums, lo, hi);
/************************/
sort(nums, lo, p - 1);
sort(nums, p + 1, hi);
}
先构造分界点,然后去左右子数组构造分界点,你看这不就是一个二叉树的前序遍历吗?
堆排序(重点)
堆排序是选择排序的优化,选择排序需要在未排定的部分里通过「打擂台」的方式选出最大的元素(复杂度 O(N)),而「堆排序」就把未排定的部分构建成一个「堆」,这样就能以O(logN) 的方式选出最大元素;
堆是一种相当有意思的数据结构,它在很多语言里也被命名为「优先队列」。它是建立在数组上的「树」结构,类似的数据结构还有「并查集」「线段树」等。「优先队列」是一种特殊的队列,按照优先级顺序出队,从这一点上说,与「普通队列」无差别。「优先队列」可以用数组实现,也可以用有序数组实现,但只要是线性结构,复杂度就会高,因此,「树」结构就有优势,「优先队列」的最好实现就是「堆」
堆排序 原理:
* 先将无序数组构成一个二叉堆(完全二叉树),使得每个节点都小于其子节点(兄弟节点之间可以无序),
* 每次取出根节点后,重新调整堆使其仍满足上述特性,每次取出的根节点就构成了一个有序数组。
* 建堆
* 用数组来存放整个二叉堆,且数组的首元素中存储着整个二叉堆的节点总个数,
* 建堆时总是在二叉堆的叶子节点处插入新节点,然后“上浮”该节点,最终在数组中得到一个二叉堆。
* 调整堆
* 每次在叶子节点处插入新节点即在数组末位插入新元素需要调整堆,比较新节点和其父节点的大小,
* 通过不断交换使其“上浮”,并最终位于二叉树的合适位置;
* 每次取出二叉堆的根节点即取出数组的第二个元素后需要调整堆,将二叉树的最后一个叶子节点作为新的根节点,
* 然后依次比较其和子节点的大小,通过不断交换使其“下沉”,并最终位于二叉树的合适位置
public static int[] sortArray(int[] nums) {
int len = nums.length;
// 将给定的数组整理成堆
heapify(nums);
//数组第一次整理成堆以后,堆顶就是数组的最大值
// 循环不变量:区间 [0, i] 堆有序
for (int i = len - 1; i >= 1; ) {
// 把堆顶元素(当前最大)交换到数组末尾
swap(nums, 0, i);
// 逐步减少堆有序的部分
i--;
// 下标 0 位置下沉操作,使得区间 [0, i] 堆有序,将剩下的数组在整理成大根堆,继续将堆顶元素交换到数组末尾i上
siftDown(nums, 0, i);
}
return nums;
}
/**
* 将数组整理成堆(堆有序)
*
* @param nums
*/
private static void heapify(int[] nums) {//给定一个数组,第一次将整个数组调整成大根堆
int len = nums.length;
// 只需要从 i = (len - 1) / 2 这个位置开始逐层下移
for (int i = (len - 1) / 2; i >= 0; i--) {
siftDown(nums, i, len - 1);
}
}
/**
* @param nums
* @param k 当前下沉元素的下标
* @param end [0, end] 是 nums 的有效部分
*/
private static void siftDown(int[] nums, int k, int end) {
while (2 * k + 1 <= end) {//如果2 * k + 1 > end,则k为叶子节点,没有子节点,超过了整棵树。或者其子节点已经排序完成,不需要再调整
int j = 2 * k + 1;//k的左子树
if (j + 1 <= end && nums[j + 1] > nums[j]) {//右子树>左子树
j++;
}
if (nums[j] > nums[k]) {//现在的j是左子树和右子树种较大的那个的索引
swap(nums, j, k);//如果左子树或者右子树比父节点的值 大,就将较大的值换到父节点上
} else {//否则则是已经有序
break;
}
k = j;//原来的父节点已经下沉到比他大的子节点上去了,在接着判断这个下沉的父节点还有没有比他更大的子节点
}
}
private static void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
复杂度分析:
- 时间复杂度:O(NlogN),这里 N 是数组的长度;
- 空间复杂度:O(1)
归并排序(重点)
基本思路:借助额外空间,合并两个有序数组,得到更长的有序数组。
* 将数组均分成两个子数组,先将两个子数组分别进行排序,然后合并得到全体元素都有序的数组,
* 为使上述的两个子数组分别有序,需要先对其各自的两个子数组进行排序再合并,因此需要递归地对每个子数组的两个子数组进行归并排序,直到子数组只有2个元素,此时只需要直接进行合并
分: 不断将数组从中点位置划分开(即二分法),将整个数组的排序问题转化为子数组的排序问题;
治: 划分到子数组长度为 1 时,开始向上合并,不断将 较短排序数组 合并为 较长排序数组,直至合并至原数组时完成排序
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
if (L == R) {//basecase,
return;
}
int mid = L + ((R - L) >> 1);
process(arr, L, mid);//最后就二分成两个数组分别只有一个元素
process(arr, mid + 1, R);//执行完上一个的basecase才会执行这条语句
merge(arr, L, mid, R);
}
public static void merge1(int[] arr, int left, int mid, int right) {
int[] help = new int[right - left + 1];
int i = 0;//辅助数组索引
int p1 = left;//有序数组1的索引指针
int p2 = mid + 1;//有序数组2的索引指针
while (p1 <= mid && p2 <= right) {//将两组有序的数组[left mid][mid+1,right]进行归并
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];//谁小就先把谁放入辅助数组。指针++
}
while (p1 <= mid) {//当第二个数组已经遍历完 ,直接将有序数组1放入
help[i++] = arr[p1++];
}
while (p2 <= right) {//当第1个数组已经遍历完 ,直接将有序数组2放入
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {//将辅助数组在遍历放回nums数组
arr[left + i] = help[i];
}
}
归并的两种写法,用while和if
private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
System.arraycopy(nums, left, temp, left, right + 1 - left);
//复制一个一维空数组,防止修改原数组,源数组是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) {//数组1已经归并完成
nums[k] = temp[j];
j++;
} else if (j == right + 1) {//数组2已经归并完成
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++;
}
}
「归并排序」比「快速排序」好的一点是,它借助了额外空间,可以实现「稳定排序」。
复杂度分析:
时间复杂度:O(NlogN),这里 N 是数组的长度;
空间复杂度:O(N),辅助数组与输入数组规模相当。
tips:再说说归并排序的逻辑,若要对nums[lo..hi]进行排序,我们先对nums[lo..mid]排序,再对nums[mid+1..hi]排序,最后把这两个有序的子数组合并,整个数组就排好序了。
归并排序的代码框架如下:
void sort(int[] nums, int lo, int hi) {
int mid = (lo + hi) / 2;
sort(nums, lo, mid);
sort(nums, mid + 1, hi);
/****** 后序遍历位置 ******/
// 合并两个排好序的子数组
merge(nums, lo, mid, hi);
/************************/
}
先对左右子数组排序,然后合并(类似合并有序链表的逻辑),你看这是不是二叉树的后序遍历框架?另外,这不就是传说中的分治算法嘛,不过如此呀
插入排序(熟悉)
// 插入排序:稳定排序,在接近有序的情况下,表现优异
原理: * 确保数组前i-1个元素已经排好序,在第i轮循环时,将第i个元素从后往前依次和 * 其前面的元素比较和交换,最后插入到前i-1个有序的子数组中的合适位置
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
//方法1
for (int i = 1; i < arr.length; i++) {//从第二个元素开始调整,直到最后一个元素
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {//对i之前的所有元素进行遍历判断
swap(arr, j, j + 1);
}
//对方法1进行优化
//插入排序改进
// 如果当前元素已经位于正确的位置,则不必继续往前插入,可以提前结束本轮循环
for (int i = 1; i < a.length; ++i) {
for (int j = i; j > 0; --j) {
if (a[j - 1] > a[j])
swap(a, j - 1, j);
else
break;
}
}
}
public static 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];//将前面大于temp该值的元素依次后移
j--;//继续往前判断
}
nums[j] = temp;//当前面的元素都不在大于temp,不再满足上面的while,这时再将temp放入该放入的位置
}
return nums;
}
复杂度分析:
- 时间复杂度:O(N2),这里 N 是数组的长度;
- 空间复杂度:O(1),使用到常数个临时变量
选择排序(了解)
原理: * 从数组的首元素开始,使用i指针指向索引位置,标记为最小值,用另一个指针在剩下的元素里面遍历,如果遍历当前元素比这个最小元素大,则交换之,使得第i个位置的元素值为第i小,相当于每一轮循环是在所有未排序的元素之中选择出最小的元素
每一轮选取未排定的部分中最小的部分交换到未排定部分的最开头,经过若干个步骤,就能排定整个数组。即:先选出最小的,再选出第 2 小的,以此类推
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {//i位置找第i小的元素,最后一个位置不用找
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
复杂度分析:
- 时间复杂度:O(N2),这里 N 是数组的长度;
- 空间复杂度:O(1),使用到常数个临时变量。
冒泡排序(了解)
原理: * 每一轮循环中依次比较相邻位置上元素的大小,使得较大的元素后移, * 且确保第i轮循环完之后能把第i大的元素移动到排序后的位置上
改进: * 每一轮循环开始前设置标志位,如果本轮循环没有交换任何元素, * 则说明所有元素已经有序,可以提前结束排序
public int[] bubbleSort(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]) {
swap(nums, j, j + 1);//用j指针做内层遍历,将数组arr[j,i]中的最大值移动到i位置上,
sorted = false;
}
}
if (sorted) {
break;//已经全部排好序
}
}
return nums;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
计数排序(了解)
基数排序(了解)
桶排序(了解)
希尔排序(不要求)