复杂度和常见算法(上)
2018.1.13
1)认识复杂度以及master公式
2)常见的排序算法(插入,选择,快速,归并,堆,基数排序,桶排序)
#知识点总结#
排序算法 对数器 对数器验证贪心策略 递归问题的时间复杂度 递归 算递归程序的时间复杂度 位运算
时间复杂度
一个长度为n的数组求最大值。时间复杂度 O(n) 遍历一遍
冒泡排序 长度为n的数组。 时间复杂度O(n^2) 两个for
(a*n^2+b*n+k)*c
求时间复杂度不需要低阶项,忽略最高阶项的系数 c是内存寻址的代价,是一个常数(系数为1)
上式的时间复杂度 O(n^2) O就是阶数
所有O(n^2)的算法都可以优化成O(N*logN) 首先想到二分
设长度为M,N的两个数组归并排序
N*logM 和 M*logN 数据量没确定无法判断那个更优
可以加入一个判断,将大的值放在log后的位置上 if M>N N*logM 为优
空间复杂度
所谓空间复杂度是额外的空间复杂度,比如题目本身传入一个长度为n的数组 不算额外空间
为了支持流程需要用多少辅助空间才能完成的叫额外空间 比如交换a和b的值,需要引入一个temp变量 temp为额外空间
例题
输入 int arr[]={1,2,3,4,5,6,7};
输出 int arr2[]={6,7,1,2,3,4,5};
额外空间多的方式
创建一个等长空数组,填充,额外空间复杂度是 n
额外空间复杂度少的方式
将原数组看做两部分 1,2,3,4,5 6,7
辅助空间没有形成量级 是常数级别的 额外空间复杂度O(1) 和数据规模没有关系 逆序只需要借助一个额外空间
将两部分分别逆序
5,4,3,2,1 7,6
整体逆序
6,7,1,2,3,4,5
额外空间复杂度O(1)
除特殊声明外,最优解的概念是先满足时间复杂度最优,再满足空间复杂度最优的解。
时间复杂度相同,使用额外空间最少的是最优解。
特殊声明(例如,使用的额外空间必须是O(1) 在考虑时间复杂度)
#额外知识点# 对数器
用法1-->用于测试自己的算法是否正确
在跑oj评测系统(Online Judge)的时候 时间复杂度 O(n^2)的方法一般会挂掉
但是你可以先写一个保证正确,容易写,但是时间复杂度高的方法
比如排序算法。我想测试自己的排序方法是否是正确的。生成随机样本 我先写一个生成随机长度的数组,数组内值也是随机的一个算法
循环n次
先调用系统提供的排序方法Arrays.sort(arr)方法
然后再调用自己写的排序算法mySort(arr)方法
然后比较两个排序完成的数组值是否相同
中途有不相等的特例的时候跳出,告诉你 自定义的方法错误
如果最后一次依然正确,那么我们就视为自定义方法正确
想测方法是否真确,但找不到oj可以自己写对数器
//优化:可以多创建一个数组保存生成的随机数组,在出错的时候将这个数组打印就可以得到在何种样例下算法会出问题
用法2-->验证贪心算法
贪心算法的数学证明非常难
例题
今年暑假不AC (王道计算机考研指南例 2.12 九度教程第22题 HDU 2037)
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 35651 Accepted Submission(s): 19001
输入节目数,开始时间结束时间,求最多能看几个节目
样例输入:
12//十二个电视节目
1 3//开始时间 结束时间
3 4
0 7
3 8
15 19
15 20
10 15
8 18
6 12
5 10
4 14
2 9
0
样例输出:
5 //最多能完整看几个节目
贪心就是我脑补一种策略,然后举反例验证是否正确
有三种思路
第一种 按开始时间排
第二种 按持续时间排
第三种 按结束时间排
第三种正确,其他错误。而验证哪个贪心策略正确也可以使用对数器的技巧
写一个完全暴力的方法(完全正确,但是复杂度高)
再分别使用三种思路贪心,比较两种算法的结果,验证哪种贪心策略正确。(验证结果是以先结束排序贪心正确)
public class note1 {
//冒泡排序 时间复杂度 O(n^2) 稳定
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];//效率高于if和换值操作 1^2=3 2^2=0
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
//用于比较的排序策略
public static void comparator(int[] arr) {
Arrays.sort(arr);
}
//生成样本 随机数组
public static int[] generateRandomArray(int maxSize, int maxValue) {
//(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;
}
// 复制数组 有一个Arrays.copyOfRange(arr, start, end)复制的是下标start到end-1的数组
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;
}
//比较两个数组
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;
}
//打印输出
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();
}
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);
int[] arr3 = copyArray(arr1);//出错样本copy
bubbleSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
printArray(arr3);//打印出错样本
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
bubbleSort(arr);
printArray(arr);
}
}
//插入排序 时间复杂度均为O(n^2) 空间复杂度O(1) 稳定
//拿扑克牌 默认左边的是有序的,往里面插入到合适位置即可
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);
}
}
}
//选择排序 优化了冒泡的交换次数 时间复杂度仍然是O(n^2) 空间复杂度O(1) 稳定
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);
}
}
//插入排序 时间复杂度均为O(n^2) 空间复杂度O(1) 稳定
//拿扑克牌 默认左边的是有序的,往里面插入到合适位置即可
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);
}
}
}
//选择排序 优化了冒泡的交换次数 时间复杂度仍然是O(n^2) 空间复杂度O(1) 稳定
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);
}
}
###归并排序 空间复杂度O(N) 稳定
归并排序就是递归问题 父过程拆分成子过程
父过程进栈 栈内保存了父过程的一切信息包括行数,然后下一个子问题,下一个子问题……知道不可拆分
在这道题中表现为分成一个数字,那么默认有序,就可以开始将他们逐步排并
时间复杂度2T*(N/2) + O(N) 前一部分是递归 后半部分是合并
master公式 求递归问题的时间复杂度
T(n)=aT(n/b)+O(N^d)
发生了a次 样本量为n/b 剩下的部分
分段函数(也是选最大阶数)
if logb^a > d 时间复杂度为 O(N*logb^a)
if d > logb^a 时间复杂度为 O(N^d)
if logb^a = d 时间复杂度为 O(N^d*logN)
master公式补充内容
http://www.gocalf.com/blog/algorithm-complexity-and-master-theorem.html
注意
1,库函数中排序的实现是综合排序,比如插入+快速;比如为了稳定性,
排序算法往往是快排+堆排序
2,归并排序和快速排序,都一定存在非递归的实现
3,归并排序,存在额外空间复杂度O(1)的实现,但是非常难,你不需要
掌握 百度一篇叫做《归并排序内部缓存法》
4,归并排序的扩展,小和问题,逆序对
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(NlogN),额外空间复杂度O(logN)
空间复杂度就是用于存储基准值的 选中间选中间到最后 不稳定
经典快速排序的第一步是选择最后一个值为基准值
第二步 划分过程
<=基准值的在左边 >基准值的在右边
第三步 对两个子区间递归这个过程,直到每个区间都只有一个数排序完成
具体实现
1 5 4 9 7 6 选最后一个,也就是6作为划分值,
先创建一个小于区的下标变量min_index 再做一个下标变量
从左往右,数组中每一个值依次与划分值作对比,若小于,则小于区min_index++
若大于小于区下表初始值为-1,遍历下标为0,遍历下标从左往右遍历
遍历下标一直增长,遇见小于等于基准值的元素,小于区下标+1,
遇见大于基准值的元素不作处理(小于区下标不变),遇见小于等于
基准值的元素,该值与小于区下标+1位置的值交换,小于区下标+1,
直到遍历下标到最后一个位置,也就是基准值的位置,基准值与小于区下标+1的
位置的值做交换。
拜托大家自己点这个图片看一下方便理解,图片太大传不上来

经典快速排序,如果选取的基准值非常偏那么时间复杂度会接近O(N^2)
不要单纯的理解为基本有序的时候表现不好
优化处理是随机选一个数作为基准值,这就变成一个概率的问题,复杂度就是O(NlogN)
先随机取一个元素,与最后一个元素交换值,再进行经典快速排序就可以
#额外的知识点 #
一道很恶心的面试题,给你你个数组,让你把奇数放在左边部分,偶数放在右边部分
每个部分相对位置不变 空间复杂度O(1)的解法
实质上是快速排序,把基准值左边排小的右边排大的变成奇数左偶数右就可以
但是相对位置不变,也就是要求快速排序稳定
有一种论文级别的写法 01 stable sort 快速排序空间复杂度O(1)论文
面试的时候是不可能做出来的
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);
}
}
//划分过程(快速排序的第一步) 时间复杂度O(n) 空间复杂度O(1)
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 };
}
###堆排序 时间复杂度O(N*logN) 空间复杂度O(1)
额外空间复杂度O(1),实现不能做到稳定性
关键步骤:heapInsert, heapify,堆的扩大和缩小操作
1,堆排序中,建立堆的操作O(N)
log1+log2+log3+...+logN函数收敛 时间复杂度O(N)
2,堆排序的核心数据结构:堆,也可以说是优先级队列
堆的概念 它是一个完全二叉树 用数组表示
i的左孩子的下标为2*i+1 i的右孩子下标为2*i+2
i的父节点下标为(int)(i-1)/2
这种数据结构非常有用也非常重要
排序过程就是建堆 建完后堆顶与最后一个位置交换再把最后一个元素排除(size--)在外再调整堆
当有效size为0就排完了
额外知识点 swap(arr, 0, --size);等价于 swap(arr, 0, size); size--;
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
//建堆
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;
}
}
//调整堆 arg1是当前下标 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;
}
//某个孩子比我大,那个孩子的位置是largest
swap(arr, largest, index);//largest是某个孩子的值,交换
index = largest;
left = index * 2 + 1;
}
}
//基数排序 时间复杂度O(N),额外空间复杂度O(N) 稳定
// only for no-negative value
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
public static int maxbits(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int res = 0;
while (max != 0) {
res++;
max /= 10;
}
return res;
}
public static void radixSort(int[] arr, int begin, int end, int digit) {
final int radix = 10;
int i = 0, j = 0;
int[] count = new int[radix];
int[] bucket = new int[end - begin + 1];
for (int d = 1; d <= digit; d++) {
for (i = 0; i < radix; i++) {
count[i] = 0;
}
for (i = begin; i <= end; i++) {
j = getDigit(arr[i], d);
count[j]++;
}
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
for (i = end; i >= begin; i--) {
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for (i = begin, j = 0; i <= end; i++, j++) {
arr[i] = bucket[j];
}
}
}
public static int getDigit(int x, int d) {
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
//桶排序 时间复杂度O(N),额外空间复杂度O(N)
/*
* 1,桶排序的扩展,排序后的最大相邻数差值问题
* 2,非基于比较的排序,对数据的位数和范围有限制。
桶排序,也叫计数排序 不是基于排序的 是基于统计字频
* 0-200的数字 准备201长度的容器 如果数字与容器下标相同,该下标的计数器++
* 桶思想的算法,求最大差值
*/
// only for 0~200 value
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;
}
}
}
/*
* 位运算
* 位运算 交并补比算术运算快很多(常数级)
* a/2 等价于 a>>>1 高位补0右移
* a>>1 高位按符号位补右移
*/
一般java系统类库的sort是综合排序
size<60用插入排序 >60的分成60一份然后用归并merge或者快排quick
基本数据类型用快排
自定义数据类型用归并排序(因为稳定)