算法<初级> - 第一章 时间复杂度与排序(完结)
算法<初级> - 第一章
时间复杂度:
-
Big O
时间/空间复杂度计算一样,都是跟输入数据源的大小有关 -
n->∞
-
O(logn)
每次只使用数据源的一半,logn同理 -
最优解
先满足时间复杂度的情况最优情况下使用最小空间复杂度
例题:子序列交换
- 题目描述:
输入一个序列,如1234567,在5和6位置处分成两个子序列,12345与67,将两个序列交换输出,如该输出为6712345。 - 思路讲解:
- 首先将两个子序列逆序合并
- 逆序:左右两指针交换元素值直至中间
逆序空间复杂度:O(1) - 再将生成的新序列逆序,得到输出结果
- 时间复杂度:O(n)
- 演示:
- 子逆序合并:
12345 67
54321 76 —> 5432176 - 整体逆序:
6432175 —> 6732145 —> 6712345 - 完成√
- 子逆序合并:
排序-冒泡排序
-
思想
- 序列相邻两元素进行比较
-
演示(升序)
- 第一轮:
确定最大值/最后一位:
147523689 —> 147523689 —>
147523689 —> 145723689 —>
145273689 —> 145237689 —>
145236789 —> 145236789 —>
145236789 - 第二轮:
确定第二大值/倒数第二位:
145236789 —> 145236789 —>
145236789 —> 142536789 —>
142356789 —> 142356789 —>
145236789 —>
145236789 - 后面同理;
- 第一轮:
-
时间复杂度O(n+n-1+n-2...1)=O(n2)
- 无论好坏
-
算法实现(Java)
public static void bubbleSort(int[] arr) { //冒泡排序
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
public static void swap(int[] arr, int i, int j) { //交换
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
对数器
验证自我实现算法正确性的方式:与正确算法跑大量同样的测试用例比较结果
- 随机数发射器
用于生成大量随机的测试用例
//随机数发射器
public static int[] generateRandomArray(int maxSize, int maxValue) {
//Math.random() -> double [0,1)
//(maxSize + 1) * Math.random() -> [0,size+1] double
//(int) ((maxSize + 1) * Math.random()) -> [0,size] 整数
//生成测试用例数组
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
//随机生成数组中的数(任意方式)
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
- 用于快速验证贪心策略的正确性
- 举例:用自我实现的冒泡排序与Java库排序函数进行比较:实现对数器
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
bubbleSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
bubbleSort(arr);
printArray(arr);
}
public static void comparator(int[] arr) {
Arrays.sort(arr);
}
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
排序-选择排序
- 思想
- 序列各值与标记位置上的值进行比较
- 时间复杂度:O(n2)
- 无论好坏
- 算法实现(Java)
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
排序-插入排序
- 思想
- 从第二个(1索引)开始,跟自己之前所有比较,根据结果交换,直至停止,再继续循环
- 时间复杂度:O(n2)
- 最好情况:O(n)
- 算法实现(Java)
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
排序-归并排序
- 思想
- 递归
计算机内栈实现-现场保存变量压栈
先进后出-保证子递归值返回给父递归
递归终止条件:base case - mergeSort():递归
- 如果序列len1,或者左右,则返回 (base case)
- 序列分成左右两个子序列,对两个子序列对其调用归并排序使之有序(递归)
取中间数方法:(L+R)/2 (L+R)>>1 L+(R-L)/2 —>(防溢出)
- merge():合并 (又称外排)
- 两个指针子序列头开始,左小放左,右小放右(针对有序序列的排序合并)
- 当某边为空(越界)时,另一边剩下直接放
- 递归
- 演示
- 74583291 —> 7458 3291 —>
74 58 32 91 —> 7 4 5 8 3 2 9 1—>
47 58 23 19 —> 4578 1239 - 完成√
- 74583291 —> 7458 3291 —>
- 时间复杂度:O(N* logN)
T(n)=2T(n/2)+O(N) T(n)递归复杂度 O(N)除递归外复杂度- 空间复杂度:O(N)
- 针对空间复杂度的优化 - 归并排序的内部缓存法 — 空间复杂度O(1)
- 原地归并排序 — 空间复杂度O(1) 时间复杂度却是O(N
2)
- Master公式:计算递归类算法的时间复杂度公式
- [补充阅读]
- T(n)=aT(n/b)+O(Nd)
- 若logba > d:
时间复杂度:O(Nlogba) - 若logba < d:
时间复杂度:O(Nd) - 若logba = d:
时间复杂度:O(Nd* logN)
- 空间复杂度:O(N)
- 非递归版本思想
+ 每相邻序列之间进行外排 - 算法实现(Java)
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
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];
}
}
例题:小和问题
- 题目表述
对于某序列,将各个元素 左边比之小的数相加 ,求和各个元素的如此操作的值,称为小和,返回小和的值。
- 暴力求解
遍历-遍历
时间复杂度:O(n2) - 归并排序求解
- 归并代码
- 小和产生在外排“左小放左,右小放右”的过程中(左右组间产生小和)
- 当外排比较时,此时两边都是有序序列
- 若左小,则右边产生小和,小和值为
(左值 * 右边未放下序列的个数) - 若右小,则照旧进行
- 若左小,则右边产生小和,小和值为
res+=arr[p1] < arr[p2] ? (r-p2+1) * arr[p1] : 0 ;
-
演示
413506 —> 413 506 —>
41 3 50 6 —> 4 1 3 5 0 6 —>
41外排(不产生小和) 3外排 50外排(不产生小和) 6外排 —>
14 3外排 05 6外排 —>- 14 3产生小和:1*3=3
- 05 6产生小和:01+51=5
134 056外排产生小和:1 * 2+3 * 2+5 * 2=18
所以最终小和=3+5+18=26
排序-快速排序
经典快排(partition)
- 取最后一个数做基准
- 设一个 小于区 ,边界索引为-1 (大于区同理)
- 开始遍历序列
- 若元素小于(等于)基准,则将该元素与 边界索引+1 的位置交换,并将边界索引设置为 交换后该元素位置索引 —>等于是为了将最后位置的基准放到中间区
- 若元素大于基准,则继续遍历下一个元素
- 演示
- 0351674 —> 0351674(边界-1)—>
- 0351674(边界0)—> 0351674 (边界1) —>
- 0351674 (边界1) —> 0351674 (边界2) —>
- 0315674(边界2)—> 0315674 (边界2) —>
- 0315674(边界3) —> 0314675
- 算法实现(Java)
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[] { less + 1, more };
}
例题:荷兰国旗问题
- 题目表述
给定一个整数数组,给定一个值K,这个值在原数组中一定存在,要求把数组中小于K的元素放到数组的左边,大于K的元素放到数组的右边,等于K的元素放到数组的中间,最终返回一个整数数组,其中只有两个值,分别是等于K的数组部分的左右两个下标值。 - 实际上就是一个经典快排的改编(大于区+小于区)
- 一开始基准归于大于区,大于区判定完后判定小于区,等于不进行操作继续遍历,遍历完后大于区边界与基准交换位置。
随机快排
- 思想
- 随机选择序列中一个数,首先将其跟序列最后一个数进行交换,然后对该序列进行一次partition经典快排,返回 小于区边界与大于区边界索引
- 之后对 (序列左边界 - 小于区边界-1) 和 (大于区边界+1 - 序列右边界) 进行随机快排调用递归
- 终止条件basecase:L>=R时,返回该序列
- 时间复杂度
- 概率长期期望O(NlogN):实际工程非常好,因为排序常数项很少
- 最优O(NlogN)
- 最差O(N2):每次随机基准选择都是序列最值
- 空间复杂度 O(logN):记录每次的排序完后基准位置
- 算法实现(Java)
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1, r);
}
}
排序稳定性
-
概念
无序序列排成有序序列后,位置相对次序保持不变 —> 排序算法具有稳定性 -
可以实现稳定性版本的排序算法
冒泡排序 - 插入排序 - 归并排序 -
不可以实现稳定性版本的排序算法
选择排序 - 快速排序(01stable sort可以实现稳定性)
面试坑题:按奇偶划分左右-次序不变
-
题目表述
一个序列,将序列中奇数放在左边,偶数放在右边,且保证奇偶数子序列次序不变,返回新序列,要求额外空间复杂度O(1) -
实际上是一个0-1博弈,对一个元素的判定非0即1
-
大小 / 奇偶 都是0-1博弈的一种判定机制而已
-
结合题目,实际上就是问实现 稳定性版本快排问题 —— 着重点就是次序不变
-
要求 01stable sort论文级别算法 - 面试坑题
排序-堆排序
-
堆结构
- 堆是一棵完全二叉树(数组可以对应完全二叉树/堆结构)
- N结点,高度=logN
- i结点:左孩子结点2i+1 / 右孩子结点2i+2 / 父亲结点(i-1)/2 (地板除)
- 堆是一棵完全二叉树(数组可以对应完全二叉树/堆结构)
-
大根堆:堆每棵子树的头节点都是最大值(小根堆同理)
- Java中优先队列默认是由小根堆构成,可以用比较器自定义优先级
-
堆构造大根堆算法:heapinsert()过程
- 经常用于解其他堆类题目
- 遍历每个数
- 与其父节点((i-1)/2)进行比较,如果大于父节点,则两个进行交换;交换后再与新父节点进行比较,直至停止(while)
- 遍历完后堆就变成了大根堆,小根堆构造同理
- 大根堆构造完后,形成上大下小的结构,但是无序
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
- 大根堆有序化算法:heapify()过程
- 先将大根堆头节点跟最后一个结点交换(最大值固定,size-1)
- 遍历除最后结点外所有的节点
- 如果小于子节点最大值,则与最大值位置进行交换;交换后再与新子节点进行比较,直至停止(while)
- 遍历完后堆就变成了从小到大的有序堆 - 堆排序
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) { //左边不越界
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left; //找到两个孩子中最大的;如果只有一个左孩子,就返回左孩子
largest = arr[largest] > arr[index] ? largest : index; //孩子最大值与自己比较
if (largest == index) { //自己节点最大
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
- 堆排序两个过程
- 调整堆向上走:heapinsert
- 调整堆向下走:heapify
- 堆 - 大根堆 - 有序堆
- 时间复杂度:
- 建立大根堆时间复杂度:log1+log2+..logN=O(N)
- 调整大根堆时间复杂度:O(NlogN)
- 工程效果不好,原因常数项操作太多
Java_Arrays库 - Arrays.sort()系统排序
- Arrays.sort(arr)
- 当arr.size<60左右时,后台使用的是insertion_sort
原因插入排序常数项很少,在数据项少时使用效果很好 - 当arr.size>60左右时,后台使用的是merge_sort和quick_sort
- 当数据类型是int / char / double时,后台用quick_sort
- 当数据类型是自定义数据类型时,后台默认用merge_sort(可以使用自定义比较器)
- 两者区别使用的原因:排序稳定性
- 基础类型不要求稳定性,只要求快(常数项比merge_sort少)
- 自定义类型要求排序稳定性,现实世界有原始数据次序往下传的需求
- 当arr.size<60左右时,后台使用的是insertion_sort
比较器
-
对两个或多个数据项进行比较,以确定它们是否相等,或确定它们之间的大小关系及排列顺序称为比较。 能够实现这种比较功能的电路或装置称为比较器。
-
定义比较器 / 重新定义比较函数
- 其中一种继承Comparator接口的比较器实现方法
public static class IdDescendingComparator implements Comparator<Student> {
@Override //传入student类
public int compare(Student o1, Student o2) { //比较student类中的id属性
return o2.id - o1.id; //return>0,则第一个参数排序在前;否则第二个参数排序在前
}
}
Arrays.sort(students, new IdAscendingComparator());
printStudents(students);
排序-桶排序
- 与之前的算法不同,排序过程与比较无关,基于数据状况(数据唯一值越少越好)
- 思想(桶排序是一种思想)
- 数据序列N个,唯一数据种类n个,准备n个容器
- 遍历数据,放入对应数据类容器
- 遍历完后再将容器倒出,排序完成
- 时间复杂度:O(N),空间复杂度:O(N)
- 可以用桶排序去减少时间开销,当空间内存富裕的时候
- 常见的两种桶排序方法
- 计数排序
- 基数排序
- 算法实现(Java)
public static void bucketSort(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;
}
}
}
计数排序
- 生成0-max索引的数组
- 遍历序列,遇到某值则数组某值索引++
- 遍历数组,有多少数则返回多少个对应索引值
例题:最大相邻数差
-
题目表述
- 输入一个无序数组,元素long类型,正负0都可以;输出有序后相邻两元素之间的最大差值,要求时间复杂度O(N)
-
思想
- 不使用正常排序,想到桶排序,因为时间复杂度O(N);也不适用计数排序,因为元素状况
- 数组个数N个,准备N+1个桶,将该数组中最小-最大值范围等分成N+1份,对应N+1个桶
- 将数组元素根据划分范围放入桶中,最小桶和最大桶一定不为空,中间桶一定有一个空桶
- 核心:
- 离空桶左右最近的两个桶,左桶的最大值跟右桶的最小值,一定相邻,并且差值一定>=一个桶的范围;所以,我们不用关心一个桶内部相邻情况,只需要知道空桶左右最值相差情况。
- 所以在数据放入桶的时候,只需要记录桶的最大值/最小值,其他值都不需要存储,遍历完后将找到所有空桶,比较各空桶相邻的两数差即可
-
演示
- 3150(-9)8(0.5) 范围(-9 - 8)等分成(7+1)份 有8个桶
- -9 空 空 空 0,0.5,1 3 5 8
- 只保留各个桶的最值情况,找到空桶(连续空桶可以跳过),比较空桶相邻两数差
-
算法过程
- boolean min max数组各n+1个元素(建桶)
- 序列元素进桶,记录桶号
- boolean设置true,比较该桶最大小值
- 遍历每一个非空桶,用(上一桶的最大值-本桶的最小值)与全局比较,得到结果
public static int maxGap(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
int len = nums.length;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i = 0; i < len; i++) {
min = Math.min(min, nums[i]);
max = Math.max(max, nums[i]);
}
if (min == max) {
return 0;
}
boolean[] hasNum = new boolean[len + 1]; //建桶
int[] maxs = new int[len + 1];
int[] mins = new int[len + 1];
int bid = 0;
for (int i = 0; i < len; i++) {
bid = bucket(nums[i], len, min, max); //放桶
mins[bid] = hasNum[bid] ? Math.min(mins[bid], nums[i]) : nums[i]; //更新
maxs[bid] = hasNum[bid] ? Math.max(maxs[bid], nums[i]) : nums[i];
hasNum[bid] = true;
}
int res = 0;
int lastMax = maxs[0];
int i = 1;
for (; i <= len; i++) { //得到结果
if (hasNum[i]) {
res = Math.max(res, mins[i] - lastMax);
lastMax = maxs[i];
}
}
return res;
}
public static int bucket(long num, long len, long min, long max) {
return (int) ((num - min) * len / (max - min));
}