周六参加孤尽老师的柚子班,有一个独特的环节:在纸上手写冒泡排序、插入排序、快速排序,要求15分钟写完,且把代码写到IDE里面能编辑通过且运行正确,正确一个算一分,结果现场21人能得分的只有4人,本人也是一分未得,感觉很羞愧🤦♂️。后面孤尽老师秀了一手:在txt文件编辑器中写快排,然后copy到IDE运行一把通过,赢得了在场阵阵掌声。孤尽老师本人跟大家说,他自己其实没有多少时间去记快排的代码(又要給我们准备讲课的内容,还有工作的事情),但是已经理解通透原理,所以手写出来不成问题。
柚子班第一期就讲过学习四部曲:记忆、理解、表达然后融汇贯通。排序算法之前其实都记过,但是并没有理解通透,才会忘记,借着这次机会,我决定好好写一写这几种排序算法。
1、冒泡排序
冒泡排序每趟会从左到右依次比较当前位置和下一个位置的两个数A、B,如果A比B大,则交换位置,这样每趟比较过后,最大的数字会被移到最后面,数组长度为n,总共需要比较 n-1趟,用i(从1开始)来表示第几趟比较,每趟需要比较 n-i 次。
public static void sort(int[] nums) {
if (nums==null || nums.length<2) {
return ;
}
//比较n-1趟
for (int i=1; i<nums.length; i++) {
boolean flag = true;
//每趟比较n-i次,从前往后开始比较
for (int j=0; j<nums.length-i; j++) {
if (nums[j]>nums[j+1]) {
int temp = nums[j];
nums[j]=nums[j+1];
nums[j+1]=temp;
flag = false;
}
}
//如果一趟比较过后都没有交换过,说明数组已经有序,可以直接返回
if (flag) return;
}
}
平均时间复杂度:
空间复杂度:O(1)
2、插入排序
插入排序和玩斗地主的时候,給扑克牌排序很相似,从左到右,給每张牌找到合适的位置。先把当前要排序的牌A抽出来,和前面的牌一一比较,如果前面的牌比较大则前面的牌依次往后移动,直到遇到前面的牌比A小,则把A放在它后面。給从第二张牌开始到最后一张牌找到合适的位置,需要走n-1趟,每趟需要比较i次。
public static void sort(int[] nums) {
if (nums==null || nums.length<2) {
return;
}
//从第2张牌到最后一张牌进行每趟比较
for (int i=1;i<nums.length;i++) {
//先把当前要比较的牌a抽出来
int a = nums[i];
//和前面i-1张牌依次比较
int j = i-1;
for (;j>=0;j--) {
//如果前面的牌比当前的牌a大,则后移
if (a < nums[j]) {
nums[j+1]=nums[j];
} else {
//否则退出
break;
}
}
//退出的牌的后一位,就是合适牌a的位置
nums[j+1]=a;
}
}
平均时间复杂度:
空间复杂度:O(1)
3、快速排序
快速排序是一种基于分治思想的算法,选数组第一个值作为枢轴,重复这个循环:先让最后面的值和枢轴比较大小,如果值比枢轴大,则位置前移(indexRight--),否则和枢轴交换位置;接着让前面的值和枢轴比较,如果值比枢轴小,则位置后移(indexLeft++),否则和枢轴交换位置;直到前后位置相等(indexLeft==indexRight)。每次交换位置都是和枢轴交换,目的是让所有比枢轴小的值移动到枢轴左边,否则移动到右边。最后枢轴的位置就在中间,以枢轴所在位置分割为两个数组,递归。
写递归算法有一个小技巧,递归模版写出来了基本就对了一半:
1、在特定条件下直接return;
2、每次递归的参数在变化
public static void sort(int[] nums, int left, int right) {
//递归模版 : 1、特定条件下return
if (nums == null || left>=right) {
return;
}
int pivot = nums[left];
int indexLeft = left;
int indexRight = right;
while(indexLeft<indexRight) {
while(indexLeft < indexRight && nums[indexRight]>= pivot) {
indexRight--;
}
swap(nums, indexLeft, indexRight);
while(indexLeft < indexRight && nums[indexLeft]<= pivot) {
indexLeft ++;
}
swap(nums, indexLeft, indexRight);
}
//递归模版 : 2、参数在变化
sort(nums, left, indexLeft-1);
sort(nums, indexLeft+1, right);
}
private static void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
平均时间复杂度:O(n*lgn)
空间复杂度:O(1)
快速排序是一种不稳定的排序算法,即数组有两个相等的数值a,b,排序前a在b前面,排序之后a可能在b后面。当数组是有序的情况下(不管是升序还是降序或者值都相等),每次枢轴的位置都在边界,这样每次都只能拆分出一个成员,时间复杂度退化为
4、归并排序
归并排序,同样是利用分治的思想,将数组均分为两份,两份数组递归进行归并操作,最后将归并好的两份数组合并为一个有序的数组。
归并排序递归的模版:
1、return条件是分成的数组长度<=1
2、参数的left和right边界,按照中间拆分数组的原理变化。
public static void sort(int[] nums) {
//避免频繁创建空间
int[] temp = new int[nums.length];
sort(nums, 0, nums.length-1, temp);
}
private static void sort(int[] nums, int left, int right, int[] temp) {
//递归模版:1、return条件是数组长度<=1的时候
if (nums == null || left >= right) {
return;
}
//均分为两个数组
int mid = (left + right)/2;
//递归模版:2、参数在变化,从中间分为了两个数组
sort(nums, left, mid, temp);
sort(nums, mid+1, right, temp);
//合并
merge(nums,left,mid,right, temp);
}
public static void merge(int[] nums, int left, int mid, int right, int[] temp) {
int i=left;
int j = mid+1;
int k = left;
while(i<=mid && j<=right) {
if (nums[i]<nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++]=nums[j++];
}
}
while(i<=mid) {
temp[k++]=nums[i++];
}
while(j<=right) {
temp[k++]=nums[j++];
}
for (int ii=left;ii<=right;ii++) {
nums[ii] = temp[ii];
}
}
时间复杂度:O(n*lgn)
空间复杂度:O(n)
归并排序是稳定的排序算法,且时间复杂度最好、最坏的情况下都是一样的O(n*lgn)。
5、选择排序
选择排序的思路是从未排序的数组中选出最小的,放到未排序数组的最前面。
需要进行n-1趟,每趟做n-i次比较。
public static void sort(int[] nums) {
if (nums==null ||nums.length<2) {
return;
}
for(int i=0;i<nums.length-1;i++) {
int min =i;
for (int j=i+1;j<nums.length;j++) {
if (nums[j]<nums[min]) {
min =j;
}
}
int temp = nums[min];
nums[min]=nums[i];
nums[i]=temp;
}
}
时间复杂度
空间复杂度O(1)
选择排序很简单且稳定,时间复杂度永远是O(n2)
6、堆排序
所谓的堆是利用完全二叉树的结构来维护的一维数组。
比如下面这个数据用堆来表示:
父的位置i,对应子的位置分别是2*i+1、2*i+2
堆又分为
大顶堆: 每棵子树根的值都是树里面所有节点最大的。
小顶堆: 每棵子树根的值都是树里面所有节点最小的。
堆排序就是利用堆结构来排序的算法,步骤如下:
(1)建堆。
(2)堆的根节点和堆的最后一个节点交换位置,堆的长度减1(即把最大(小)的值移动到数组后面)
(3)重新调整堆,重复第二步,直到堆长度为1。
public static void sort(int[] nums) {
if (nums==null || nums.length<2) {
return;
}
// 1、建堆
buildMaxHeap(nums);
//每次堆的长度减1,直到堆只有一个元素
for(int i=nums.length-1;i>0;i--) {
//2、堆顶和堆的最后一个元素交换
swap(nums,0,i);
//3、调整堆
heapify(nums,0,i);
}
}
/**
建堆思路:
从最后一个父节点:nums.length-1/2 开始进行调整堆的操作,
一直到调整到第一个节点。
**/
private static void buildMaxHeap(int[] nums) {
for(int i=(nums.length-1)/2;i>=0;i--) {
heapify(nums,i,nums.length);
}
}
/**
调整堆的思路很简单:
当前节点和左右子节点比较,找出最大的,
如果最大的不是父节点,则交换父节点和最大的子节点的值,
然后重新调整被交换的子堆
**/
private static void heapify(int[] nums, int i, int length) {
int largest = i;
int left = 2*i+1;
int right = 2*i+2;
if (left < length && nums[left]>nums[largest]) {
largest = left;
}
if (right < length && nums[right]>nums[largest]) {
largest = right;
}
if (largest == i) {
return;
}
swap(nums,i,largest);
heapify(nums, largest, length);
}
private static void swap(int[] nums, int i, int i1) {
int temp = nums[i];
nums[i] = nums[i1];
nums[i1] = temp;
}
堆排序是一种不稳定的排序。
平均时间复杂度:O(n*lgn)
空间复杂度:O(1)