面试算法系列- 01排序算法总结

1 选择排序

步骤:

1在序列中找到最小(大)元素,存放到排序序列的起始位置。

2 从剩余序列中继续寻找最小(大)元素,然后放到已排序序列的末尾。

3 重复第二步,直到所有元素均排序完毕。(由于存在交换,所以不稳定)

img

时间复杂度:O(n^2)

额外空间复杂度:O(1)

示例:

public static void selectionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// 0~n-1
		// 1~n-1
		// 2~n-1
		for (int i = 0; i < arr.length - 1; i++) { // i ~ N-1
			// 最小值在哪个位置上  i~n-1
			int minIndex = i;
			for (int j = i + 1; j < arr.length; j++) { // i ~ N-1 上找最小值的下标 
				minIndex = arr[j] < arr[minIndex] ? j : minIndex;
			}
			swap(arr, i, minIndex);
		}
	}

2 冒泡排序

步骤:

1 从头开始比较相邻的两个元素,小的数字放到前面的位置(一轮结束后已经确定出最大的数)

2 重复上一步骤直到达到已排序数的位置

img

时间复杂度:O(n^2)

额外空间复杂度:O(1)

示例:

public static void bubbleSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int e = arr.length - 1; e > 0; e--) { // 0 ~ e
			for (int i = 0; i < e; i++) {
				if (arr[i] > arr[i + 1]) {
					swap(arr, i, i + 1);
				}
			}
		}
	}

3 插入排序

步骤:

1 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

2 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。保证稳定性)

img

时间复杂度:O(n^2)

额外空间复杂度:O(1)

示例:

public static void insertionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// 0~0 有序的
		// 0~i 想有序
		for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序
			
			// arr[i]往前看,一直交换到合适的位置停止
			// ...(<=)  ?       <- i
			for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
				swap(arr, j, j + 1);
			}
		}
	}

4 归并排序

举例:[ 1 3 2 4]

​ 递归过程

1、 f(0,3)

2、 f(0,1) f(2,3)

3、 f(0,0) f(1,1) f(2,2) f(3,3)

image-20201104160945225

base case L == R

merge(0,3) 归并过程

使用help申请的额外空间数组, 用来将二分排序的数组整体排序.

排序步骤:

1 把无序数组从中间分为成两部分(左组和右组)

2 左组和右组分别排好序(使用递归)

3 合并左组和右组

合并步骤:

1 使用两个指针分别指向左右组的第一个数

2 比较两个指针所指的数字:

​ 2.1 指针所指的小于等于指针所指的, 拷贝左边的数到help数组,左指针加一;

​ 2.2 指针所指的大于指针所指的, 拷贝右边的数到help数组,右指针加一。 直到某一边的指针到达该分组的右边界

3 直接将剩余未比较的数拷贝的help数组

特点: 涉及比较、统计某一个数的左右两边数的分布,可以使用归并排序的思路:比如,求小和问题逆序对问题

时间复杂度:O(n*log(n))

额外空间复杂度:O(n)

示例:

public static void merge(int[] arr, int L, int M, int R) {
    int[] help = new int[R - L + 1];
    int i = 0; //给help使用的 
    int p1 = L;
    int p2 = M + 1;
    while (p1 <= M && p2 <= R) { // p1 和p2都不越界的时候,拷贝数组
        help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]; // 拷贝哪边,哪边的指针就加一
    }
    /*
		以下两个一定只会发生一个
		*/
    while (p1 <= M) {
        help[i++] = arr[p1++];
    }
    while (p2 <= R) {
        help[i++] = arr[p2++];
    }
    for (i = 0; i < help.length; i++) {
        arr[L + i] = help[i];
    }
}

// 递归方法实现
public static void mergeSort1(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) { //base case 最小的规模,不需要调用; 
        return;
    }
    int mid = L + ((R - L) >> 1); //防止溢出
    process(arr, L, mid);
    process(arr, mid + 1, R);
    merge(arr, L, mid, R);
}

5 随机快排

以下描述中 默认L为待排序的数组的左边界,R为待排序的数组的左边界

小于区右边界: 序号在右边界之前的(包括边界)的数均小于num

大于区左边界: 序号在左边界之后的(不包括边界)的数均大于num

partition问题: 把一列数arr按照最右边的数num分区,左边都是小于num的数,右边都是大于等于num的数。要求额外空间复杂度O(1),时间复杂度O(N)

partition步骤:

1 初始化参数,小于区的右边界序号为L - 1。

2 从左往右遍历每个数(直到倒数第二个),与num比较:2.1 小于等于num, 与小于区右边界的右一个交换位置,小于区的右边界序号**+1**;2.2 大于num,小于区的右边界序号不变

3 将最右边的数(既是用来比较那个数num)与小于区右边界的右一个交换位置。

荷兰国旗问题: 给定一个数组arr,和一个整数num。请把小于num的数放在数组的左边,等
于num的数放在中间,大于num的数放在数组的右边。

荷兰国旗问题步骤:

1 初始化参数,小于区的右边界序号为L - 1,大于区的左边界序号为R。

2 从左往右遍历每个数(直到倒数第二个),与num比较:

​ 2.1 小于num, 与小于区右边界的右一个交换位置,小于区的右边界序号**+1**,遍历序号**+1**;

​ 2.2 等于num,小于区的右边界序号不变,遍历序号**+1** ;

​ 2.3 大于num,与大于区左边界的左一个交换位置,小于区的右边界序号不变,遍历序号不变,大于区左边界序号减1

3 将最右边的数(既是用来比较那个数num)与小于区右边界的右一个交换位置。

快排1.0:使用递归不断partition(返回小于等于区序号),一次排好一个数

快排2.0:使用递归不断解决荷兰国旗问题(返回等于区边界),一次搞定一组相同的数的位置

快排3.0:随机选择一个数作为比较的num,再按照快排2.0

如果选择的数好,每次递归都是从中间分,这样的时间复杂度能达到o(n*log(n))。最坏的情况下 时间复杂度是o(n^2). 引入随机性抵消最坏的情况出现的可能性,推算出来的平均复杂度是O(n*log(n))

时间复杂度:O(n*log(n))

额外空间复杂度:O(log(n))

示例:

public static int partition(int[] arr, int L, int R) {
    if (L > R) {
        return -1;
    }
    if (L == R) {
        return L;
    }
    int lessEqual = L - 1;
    int index = L;
    while (index < R) {
        if (arr[index] <= arr[R]) {
            swap(arr, index, ++lessEqual);
        }
        index++;
    }
    swap(arr, ++lessEqual, R);
    return lessEqual;
}

public static int[] netherlandsFlag(int[] arr, int L, int R) {
    if (L > R) {
        return new int[] { -1, -1 };
    }
    if (L == R) {
        return new int[] { L, R };
    }
    int less = L - 1; // 小于区域的右边界
    int more = R;  // 大于区域的左边界
    int index = L;  // 
    while (index < more) {
        if (arr[index] == arr[R]) {
            index++;
        } else if (arr[index] < arr[R]) {
            swap(arr, index++, ++less); // 当前区域与小于区域的右一个做交换,跳下一个,小于区域右移
        } else {
            swap(arr, index, --more);
        }
    }
    // L...Less  Less...more -1 more...R-1 R
    swap(arr, more, R);  // 将右边界与大于区域第一个数交换,原因是整个过程中右边界没有移动过
    return new int[] { less + 1, more };
}

// 快排1.0
public static void quickSort1(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    process1(arr, 0, arr.length - 1);
}

public static void process1(int[] arr, int L, int R) {
    if (L >= R) {
        return;
    }
    // L..R partition arr[R]  ===>  [>arr[R] arr[R] >arr[R]]
    // 一次递归只解决一个数的位置
    int M = partition(arr, L, R);
    process1(arr, L, M - 1);
    process1(arr, M + 1, R);
}

// 快排2.0
public static void quickSort2(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    process2(arr, 0, arr.length - 1);
}

public static void process2(int[] arr, int L, int R) {
    if (L >= R) {
        return;
    }
    // 比partition的优点是一次搞定一组相同的数的位置
    int[] equalArea = netherlandsFlag(arr, L, R);
    process1(arr, L, equalArea[0] - 1);
    process1(arr, equalArea[1] + 1, R);
}

// 快排3.0
public static void quickSort3(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    process3(arr, 0, arr.length - 1);
}

public static void process3(int[] arr, int L, int R) {
    if (L >= R) {
        return;
    }
    swap(arr, L + (int) (Math.random() * (R - L + 1)), R); // 随机选择一个数
    int[] equalArea = netherlandsFlag(arr, L, R);
    process3(arr, L, equalArea[0] - 1);
    process3(arr, equalArea[1] + 1, R);
}

6 堆排序

堆结构比堆排序重要

大根堆:每一棵树最大的数都是头节点

小根堆:每一棵树最小的数都是头节点

建立大根堆的步骤(数组实现堆):

HeapInsert: 新增节点,依旧保存大根堆

1 每新加一个数都向上与他的父节点比较

2 比父节点大与父节点交换位置,继续向上比较

3 直到比父节点小就停止比较

HeapIfy:弹出父节点依旧保持是大根堆的方法

1 左右孩子比较谁大,下标给largest

2 孩子最大的与父比较,找到子夫最大的节点; 如果最大的依旧是父,则停止循环;是子则交换父子继续下沉

堆排序:

1 先让整个数组都变成大根堆结构

2 把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,
一直周而复始,时间复杂度为O(N*logN)

3 堆的大小减小成0之后,排序完成

时间复杂度:O(n*log(n))

额外空间复杂度:O(1)

示例:

private void heapInsert(int[] arr, int index) {
			// 一直往上跑 与父节点比较
			// (0-1)/2为什么时自己? ---> index不可能为0 __20201123
			while (arr[index] > arr[(index - 1) / 2]) {
				swap(arr, index, (index - 1) / 2);
				index = (index - 1) / 2;
			}
		}

private void heapify(int[] arr, int index, int heapSize) {
			//从index位置往下看,不断下沉
			//停的条件:子都不比我大; 没有子
			int left = index * 2 + 1;
			while (left < heapSize) { // 当有子的时候
			    // 左右两孩子,谁大下标给largest
				// 右胜出:1) 有右孩子 && 2) 右孩子比左孩子大
				// or 左胜出
				int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
				// 找到子父最大的下标,else后意味父是最大的 
				largest = arr[largest] > arr[index] ? largest : index;
				// 和三元运算符重复,但不能把index改成break
				if (largest == index) {
					break;
				}
				swap(arr, largest, index);
				index = largest;
				left = index * 2 + 1;
			}
		}

public static void heapSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// O(N*logN) 从下往上
//		for (int i = 0; i < arr.length; i++) { // O(N)
//			heapInsert(arr, i); // O(logN)
//		}
        // 以下为优化的堆排序,O(N)
		for (int i = arr.length - 1; i >= 0; i--) {
			heapify(arr, i, arr.length);
		}
		int heapSize = arr.length;
		swap(arr, 0, --heapSize);
		// O(N*logN)
		while (heapSize > 0) { // O(N)
			heapify(arr, 0, heapSize); // O(logN)
			swap(arr, 0, --heapSize); // O(1)
		}

7 计数排序

一般来讲,计数排序要求,样本是整数,且范围比较窄

步骤:

1 找到待排序所有数最大和最小数, 从小到大建立桶存放每个数出现次数

2 从桶中依次恢复数据

动图演示:

img

示例代码:

public static void countSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < arr.length; i++) {
			max = Math.max(max, arr[i]);
		}
		int[] bucket = new int[max + 1];
		for (int i = 0; i < arr.length; i++) {
			bucket[arr[i]]++;
		}
		int i = 0;
		for (int j = 0; j < bucket.length; j++) {
			while (bucket[j]-- > 0) {
				arr[i++] = j;
			}
		}
	}

8 基数排序

一般来讲,基数排序要求,样本是10进制的正整数

步骤:(常规方法)

1 如果是10进制就建立10个桶

2 从左到右观察数据的个位,按照桶的标号依次入桶

3 按照桶的位置(同桶先进先出),再从左到右观察数据的十位

4 重复3 直到最高位

动图演示:

img

9 总结

1 排序算法的稳定性

稳定性是指同样大小的样本再排序之后不会改变相对次序
对基础类型来说,稳定性毫无意义
对非基础类型来说,稳定性有重要意义

2 排序算法总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gpUg7Sp-1610532248073)(https://raw.githubusercontent.com/tonnwu/PicGo/master/tonnweb/2020/10/image-20201201202236896.png)]

1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是O(N*logN)
4)时间复杂度O(N*logN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。

8 基数排序

一般来讲,基数排序要求,样本是10进制的正整数

步骤:(常规方法)

1 如果是10进制就建立10个桶

2 从左到右观察数据的个位,按照桶的标号依次入桶

3 按照桶的位置(同桶先进先出),再从左到右观察数据的十位

4 重复3 直到最高位

动图演示:

[外链图片转存中…(img-1W9rakqP-1610532248072)]

9 总结

1 排序算法的稳定性

稳定性是指同样大小的样本再排序之后不会改变相对次序
对基础类型来说,稳定性毫无意义
对非基础类型来说,稳定性有重要意义

2 排序算法总结

[外链图片转存中…(img-5gpUg7Sp-1610532248073)]

1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是O(N*logN)
4)时间复杂度O(N*logN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。
5)为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值