一.数据结构:第一天
文章目录
1.时间复杂度
- 常数时间的操作:一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
- 先熟悉一个算法流程,然后写出这个算法流程中发生了多少常数操作,进而总结出常数操作数量的表达式
- 只要高阶项,不要低阶项,也不要系数
- 评价一个算法流程的好坏
- 先看时间复杂度
- 然后,分析不同数据样本下的实际运行时间,也就是“常数项时间”
2.常见排序问题
1.选择排序
-
选择排序【时间复杂度O(n^2) 空间复杂度O(1)】
for(int i = 0: i < arr.length: i++){ int minIndex = i; for(int j = i + 1; j < arr.length; j++){ minIndex = arr[j] < arr[minIndex] ? j : minIndex; } swap(arr, i, minIndex); }
2.冒泡排序
1.冒泡排序
-
冒泡排序【时间复杂度O(n^2) 空间复杂度O(1)】
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); } } }
2.交换算法
-
交换算法
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]; }
3.异或运算
-
异或运算
- 等同于无进位相加
- 0 ^N = N,N^N = 0
- 满足交换律和结合律
- a^b = b^a
- abc = a(bc)
- 一堆数异或的结果跟异或的先后顺序无关
- 注意:如果要用异或对两个数进行交换,那么一定要保证:
- 两个数的地址不同,连个数的值可以相同
- 如果要对数组中的值进行交换,则要保证两个数的下标不同
-
提取一个数最右侧的1
rightOne = eor & (~eor + 1);
-
异或解决问题:
-
如果有一堆数,里面有两种数是奇数个,其他都是偶数个,求这两种数
-
public static void printOddTimeNum2(int[] arr){ int eor = 0; for(int i = 0; i < arr.length; i++){ eor ^= arr[i]; } int rightOne = eor & (~eor + 1); int onlyOne = 0; for(int cur : arr){ if(cur & rightOne == 1){ onlyOne ^= cur; } } System.out.println(onlyOne + "" + (eor ^ onlyOne)); } }
-
3.插入排序
-
插入排序【时间复杂度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); } } }
4.二分查找
-
二分查找
-
插入点:数据状况/问题方向
-
问题
-
在一个有序数组中,查找某个数是否存在
-
在一个有序数组中,找>=某个数最左侧的位置
-
局部最小值问题
拉格朗日中值定理 if(最左侧和最右侧是否是局部最小值){ return 最左侧/最右侧; } else{ 必定存在一点的局部最小值 二分法选择中点 判断是否是局部最小值 if(yes) return 此点; else 继续向小的一侧二分查找,直至找到位置; }
-
-
5.随机数
-
随机数
Math.random() //[0,1)所有小数,等概率返回 Math.random() * N //[0,N)所有小数,等概率返回 (int)(Math.random() * N) //[0,N-1]所有整数等概率返回
-
两数取中点(防止溢出)
mid = (l + r) / 2; //一般形式,可能会溢出 mid = l + (r - l) / 2; //不会溢出的写法 mid = l + (r - l) >> 1; //更简化
6.master公式
- master公式:
T(N) = a* + O(N^d)
- 应用条件:子问题的规模一致
- 解释
- a:子问题调用的次数
- T(N/b):子问题的规模
- O(N^d):额外的花费
- 判断
- log(b,a) > d ——复杂度:O(N的log(b,a)次方)
O(N^log(b, a))
- log(b,a) = d ——复杂度:O(logN * (N的d次方))
0(N^d *logN)
- log(b,a) < d ——复杂度:O(N的d次方)
0(N^d)
- log(b,a) > d ——复杂度:O(N的log(b,a)次方)
7.归并排序
-
归并排序【时间复杂度O(N*logN) 空间复杂度O(N)】
public static void mergeSort(int[] arr){ if(arr == null || ar.length < 2){ return; } process(arr, 0, arr.length - 1); } public static void process(int[] arr, int L, int R){ if(L == R){ return; } int mid = L + ((R - L) >> 1); process(arr, L, mid); process(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++){ ar[L + i] = help[i]; } }
8.小和问题
-
小和问题
- 在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和,求一个数组的小和
public static int smallSum(int[] arr){ if(arr == null || arr.length < 2){ return 0; } return process(arr, 0, arr.length - 1); } public static int process(int[] arr, int l, int r){ if(l == r){ return 0; } int mid = l + ((r - 1) >> 1); return process(arr, 1, mid) + process(arr, mid + 1, r) + merge(arr, 1, mid, r); } public static int 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; int res = 0; while(p1 <= m && p2 <= r){ res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0; 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]; } return res; }
9.逆序对
- 逆序对
- 在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,找出逆序对的数量
- 解法同上,小和问题
10.荷兰国旗
-
荷兰国旗问题
-
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求时间复杂度O(N),额外空间复杂度O(1)。
-
一个数组 设置两个指针 一个左指针:指针以左小于num(一开始指向最左边,数组之外) 一个右指针:指针以右大于num(一开始指向最右边,数组之外) 两指针之间:等于num 然后从数组下标0开始遍历整个数组 会遇到3种情况 1.[i] < num,[i] 和左指针的下一个交换,左指针加一,i++ 2.[i] = num,i++ 3.[i] > num,[i] 和右指针的上一个交换,右指针减一,i++
-
11.快速排序
-
快速排序【时间复杂度O(N * logN) 空间复杂度O(logN)】
public static void qucikSort(int[] arr,int L,int R){ if(l == r){ return; } if(l > r - 60){ //在arr[l..r]插入排序 //O(N^2)小样本时,跑得快 //return; } 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); } } 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(ar, --more, L); } else { L++; } } swap(arr, more, R); return new int[] { less + 1, more}; }
12.堆排序
-
堆排序【时间复杂度O(N * logN) 空间复杂度O(1)】
- 总算法
public static void heapSort(int[] arr){ if(arr == null || arr.length < 2){ return; } for(int i = 0; i < arr.length; i++){ headpInsert(arr, i); } int heapSize = arr.length; swap(arr, 0, --heapSize); while(heapSize > 0){ heapify(arr, 0, heapSize); swap(arr, 0, --heapSize); } } public static void heapInsert(int[] arr, int index){ while(arr[index] > ar[(index - 1) / 2]){ swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } } public static void heapify(int[] arr, int index, int heapSize){ int left = index * 2 + 1; while(left < heapSize){ int largest = left + 1 < heapSize && 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; } }
-
如果已生成完全二叉树,然后将其改为大根堆【时间复杂度O(N)】
-
系统默认的函数
public void sortedArrDistanceLessK(int[] arr, int k){ PriorityQueue<Integer> heap = new PriorityQueue<>(); int index = 0; for(; index <= Math.min(arr.length, k); index++){ heap.add(arr[index]); } int i = 0; for(; index < arr.length; i++,index++){ heap.add(arr[index]); arr[i] = heap.poll(); } while(!heap.isEmpty()){ arr[i++] = heap.poll(); } }
13.比较器
- 比较器
- 实质就是重载比较运算符
- 可以很好的应用在特殊标准的排序上
- 可以很好的应用在根据特殊标准排序的结构上
14.桶排序(计数排序和基数排序)
-
桶排序
-
分析
- 不基于比较的排序
- 时间复杂度O(N),空间复杂度O(M)
- 应用范围有限,需要样本的数据情况满足桶的划分
-
计数排序
- 统计有多少个数,每个数的有多少个
- 然后输出
-
基数排序
-
先查找最大的数,然后将其他数位数不足的用0补齐
-
然后,从个位开始比较排序
-
然后,比较十位
-
然后,依次比较直至最高位
-
出桶入桶
-
代码
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 L, int R, int digit){ final int radix = 10; int i = 0, j = 0; int[] bucket = new int[R - L + 1]; for(int d = 1; d <= digit; d++){ in[] count = new int[radix]; for(i = L; i <= R; i++){ j = getDigit(arr[i], d); count[j]++; } for(i = 1; i < radix; i++){ count[i] = count[i] + count[i - 1]; } for(i = R; i >= L; i--){ j = getDigit(arr[i], d); bucket[count[j] - 1] = ar[i]; count[j]--; } for(i = L, j = 0; i <= R; i++, j++){ arr[i] = bucket[j]; } } }
-
-
15.常见排序算法总结
- 常见排序算法总结
- 目前没有找到时间复杂度为O(nlog2n),额外空间复杂度O(1),又稳定的算法。
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 | 复杂性 |
---|---|---|---|---|---|---|
直接插入排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 | 简单 |
希尔排序 | O(nlogn) | O(n2) | O(n1.3) | O(1) | 不稳定 | 较复杂 |
直接选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | 简单 |
堆排序(空间小) | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 | 较复杂 |
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 | 简单 |
快速排序(常数项少,推荐) | O(nlogn) | O(n2) | O(nlogn) | O(nlogn) | 不稳定 | 较复杂 |
归并排序(空间复杂度高,稳定性好) | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 | 较复杂 |
基数排序 | O(d (n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 | 较复杂 |
- 对于排序的改进
- 充分利用O(nlogn)和O(n2)排序各自的优势
- 稳定性考虑
- 额外了解
- 01 stable sort