1. 复杂度和简单排序算法
1.1 时间复杂度基本定义
- 常数操作:与数据量无关,每次都是固定时间内完成的操作,e.g. +/-/*/÷, 位运算,Array[i]。但是Linkedlist.get(i) 就不是,因为要遍历,数据量不同,遍历时间不同。
- 时间复杂度:常数操作数量的指标-- Big O()
- 忽略低阶,只要最高阶项,且忽略最高阶项系数
- 评价算法好坏,先看时间复杂度指标,再分析不同数据样本下的实际运行时间(常数项时间)
- 算法流程按最差情况估计时间复杂度
1.2 排序算法
1.2.1 选择排序 selection
时间复杂度O(), 额外空间复杂度O(1)
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
public static void selectionSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(i = 0; i < arr.length; i++){
int minIndex = I;
for (int j = 0; j < arr.length; j++){
if(arr[j] < arr[minIndex]){
minIndex = j;
}
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- 时间复杂度O(N^2):
- 对每个数的for loop: N+N-1+N-2+N-3+....+1 -> O()
- 每一个数都要跟后面所有的数比: N+N-1+N-2+N-3+....+1 -> O ()
- 交换: 1+1+1+1...+1 -> O(N)
- 额外空间复杂度O(1)
- 创建了新变量i,j,minIndex
1.2.2 冒泡排序 bubble
时间复杂度O(), 额外空间复杂度O(1)
比较相邻的元素。如果第一个比第二个大,就交换他们两个。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
public static void bubbleSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i=0;i<arr.length;i++){//外层循环控制排序趟数
for(int j=0;j<arr.length-i;j++){//内层循环控制每一趟排序多少次
if(arr[j]>arr[j+1]){
swap(arr, i, i+1)
}
}
}
}
public static void swap(int[] arr, int i, int j){
//不需要额外空间,交换值
// 前提,a,b在内存中不同, 内存相同会把此处的值变成0,因为N^N=0
//int a = 甲 int b = 乙
//array中使用,i和j不能是同一位置
arr[i] = arr[i] ^ arr[j];// a = a^b; a = 甲^乙, b= 乙
arr[j] = arr[i] ^ arr[j];// b = a^b; a = 甲^乙, b= 甲^乙^乙 = 甲^(乙^乙) = 甲^0 = 甲
arr[i] = arr[i] ^ arr[j];// a = a^b; a = 甲^乙^甲= 甲^甲^乙 = 0^乙= 乙, b= 甲
}
1.2.2.1 异或运算exclusive OR (xor)
- basic: 1^1 = 0, 1^0 =1, 0^0=0
- 可以看作不进位相加
- 0^N = N, N^N = 0
- 满足交换结合率:a^b = b^a, (a^b)^c = a^(b^c)
- a^b^c^d....^z 顺序不影响结果
- 例题1:int数组中,只有一个数出现奇数次,其他所有数都出现偶数次,怎么找到一个出现奇数次的数?要求时间O(N),额外空间O(1)
public static void findOddnumber(int[] arr){
int eor = 0;
for(int cur : arr){//遍历array
eor = eor ^ cur;//偶数会自己消除,奇数剩下
}
//for (int i =0; i< arr.length-1; i++){
// eor = eor ^ arr[i];}
Ststem.out.println(eor);
}
- 例题2: int数组中,有两个数出现奇数次,其他所有数都出现偶数次,怎么找到两个出现奇数次的数?
public static void findOddnumber2(int[] arr){
int eor = 0;
for(int cur : arr){
eor = eor ^ cur;//偶数会自己消除,奇数剩下
}
//eor = a^b
// eor != 0, 因为a!=b
//eor 必有一个位置是1
int rightOne = eor & (~eor+1)// 提取一个非零数最右边的1
int onlyone = 0;
//用这一位的1跟所有元素异或,偶数全都消除,只剩下a 或b。因为a,b这一位一定是不同的
for (int cur:arr){
if((cur & rightOne) == 0){
onlyOne = onlyOne ^cur; // a or b
}
}
Ststem.out.println(onlyOne + " "+ (eor ^ onlyOne));
}
1.2.3 插入排序 insertion
时间复杂度O(), 额外空间复杂度O(1)
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
public static void insertSort(int[] arr){
if (arr == null || arr.length < 2){
return;
}
for (int i = 1; i < arr.length; i++){// 0~i 范围有序
for (int j = i-1; j > = 0 && arr[j] > arr[j+1]; j--){//当前数往左换到不能再换为止,小的在左
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
1.2.4 归并排序 merge --补充2,4
时间复杂度O(N logN), 额外空间复杂度O(N)
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法: 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法)或自下而上的迭代
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤 3 直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
public static void mergeSort(int[] arr, int L, int R){
if(L == R){
return;
}
int mid = L + ((R - L) >> 1);
mergeSort(arr, L, mid);//左边有序
mergeSory(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){//判断p1,p2是否越界
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= M){//p1没越界
help[i++] = arr[p1++];
}
while (p2 <= R){//p2没越界
help[i++] = arr[p2++];
}
//两个while只能中一个,p1和p2只能有一个没越界
for(i = 0; i < help.length; i++){//排序好拷贝回原array
arr[L + i] = help [i];
}
}
应用master公式: T(N) = 2*T(N/2) + O (N) 注:merge时间复杂度 O(N) a = 2, b = 2, d =1 时间复杂度O(N logN)
- 好处:没有浪费比较行为,左右两边变成了整体有序的array,之后再合成更大的array
1.2.4.1 小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。例如: 对于数组[1,3,4,2,5] 1左边比1小的数,没有; 3左边比3小的数,1; 4左边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、2; 所以小和为1+1+3+1+1+3+4+2=16
思路:逆向思维--> 一个数右边有几个数比它大。使用归并排序
public static int smallSum(int[] arr){
if(arr == null || arr.length < 2){
return 0;
}
return process(arr, 0, arr.length - 1);
}
//arr[L...R] 既要排好序,也要求小和
public static int process(int[] arr, int L, int R){
if (L == R){
return 0;
}
int mid = L + ((R - L) >> 1);
//return左边小和+右边小和+合并时小和
return process(arr, L, mid) + process(arr, mid + 1, R) + merge (arr, L, 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){//p1没越界
help[i++] = arr[p1++];
}
while (p2 <= R){//p2没越界
help[i++] = arr[p2++];
}
//两个while只能中一个,p1和p2只能有一个没越界
for(i = 0; i < help.length; i++){//排序好拷贝回原array
arr[L + i] = help [i];
}
return res;
}
1.2.4.2 逆序对
在一个array中,左边的数如果比右边的数大,则这两个数构成一个逆序对,求逆序对数量 举个例子(前提假设升序为有序),数组[1,2,3],逆序对的数目为零;而数组[3,2,1]的逆序对就是3,分别为(3,2)、(3,1)和(2,1)
1.2.4.3 二分查找
- 有序数组中,查找某个数是否存在:时间复杂度O(logN)
- 在有序数组中,找>= 某个数最左侧的位置:时间复杂度O(logN)
- 局部最小值:无序array,相邻数一定不想等,求局部最小值
1.2.4.4 递归及master公式
1.求arr[L...R]范围中求最大值
public static int getMax(int[] arr){
return recursion(arr, 0, arr.length -1);
}
//arr[L...R]范围中求最大值
public static int recursion(int[] arr, int L, int R){
if(L == R){
return arr[L];
}
int mid = L + ((R - L) >> 1); //求中点: `mid = (L + R) / 2` 内存有可能溢出,改为 `mid = L + (R - L)/2` 优化为 `mid = L +((R - L) >> 1)` 右移一位,相当于除2
int leftMax = recursion(arr, L, mid);
int rightMax = recursion (arr, mid + 1, R);
return Math.max (leftMax, rightMax);
}
2. 递归行为时间复杂度的估算:
master公式: 子问题规模相等
T(N) = a*T(N/b) + O (N^d)
- a = 子问题调用次数
- N/b = 子问题数据规模占总规模的多少(每次递归要相等)
- O (N^d) = 分解合并的时间复杂度 解:
- logb a < d, 时间复杂度为O(N^d)
- logb a > d, 时间复杂度为O(N^(logb a))
- logb a = d, 时间复杂度为O((N^d)*log N)
1.2.5 快速排序 quick
时间复杂度O()
- 从数列中挑出一个元素,称为 "基准"(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
1.2.5.1 快排1
选最末的数为pivot,分两个区域,一个是小于这个数的,一个是大于这个数的,每次完成后把最末的数和大于区域的第一个数交换位置。之后对两个区域继续相同操作
1.2.5.2 快排2
选最末的数为pivot,分三个区域,一个是小于这个数的,一个是大于这个数的,一个是等于这个数的,每次完成后把最末的数和大于区域的第一个数交换位置。之后对两个区域继续相同操作
- 时间复杂度最差O():123456789,本身有序,pivot选最后,每次partition只能搞定一个位置
1.2.5.3 快排3
随机选一个数为pivot,分三个区域,一个是小于这个数的,一个是大于这个数的,一个是等于这个数的,每次完成后把最末的数和大于区域的第一个数交换位置。之后对两个区域继续相同操作 时间复杂度:O(N * log N)
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);//返回array长度一定为2,表示划分值等于此数的左边界和右边界
quickSort(arr, L, p[0] - 1);//< 区
quickSorr(arr, p[1] + 1, R);//> 区
}
}
//处理 arr[1...R]函数
//默认用arr[R] 划分,为三个区域,<arr[R] , ==arr[R], > arr[R]
//返回等于区(左边界,右边界),长度为2的数组res,res[0]等于区左边界,res[1]等于区右边界
public static int[] partition(int[] arr, int L, int R){
int less = L - 1;//<区右边界
int more = R;//>区左边界
while (L < more){//L表示当前数的位置
if(arr[L] < arr[R]){//arr[R] 是pivot
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};
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
1.2.6 堆排序 heap
时间复杂度O(N log N),额外空间复杂度O(1)
- 把所有数变成max-heap
- 把第一个数(一定是最大值)根最后一个数交换
- heapsize --,这个数与heap断链
- 重复1-3 直到排序完成
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for (int i = 0; i < arr.length; i ++){//O(N)
heapInsert(arr,i)//O(logN)
}//把数组排列成max-heap
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while(heapSize > 0){
heapify(arr, 0, heapSize);//O(logN)
swap(arr, 0, --heapSize);//O(1)
}
}
优化: 在数组转max-heap时,原来是自上而下,使用heap insert。优化为先把叶节点变成max-heap,然后自右向左,自下而上。时间复杂度O(N)
for (int i = arr.length -1; i >= 0; i --){//O(N)
heapify(arr, i, arr.length);
}
1.2.6.1 堆 heap
-
完全二叉树结构:满的二叉树,或从左到右正在变满的二叉树(符合BFS遍历顺序)
-
array转完全二叉树:
- arr[i] 左孩子:
2 * i + 1 - arr[i] 右孩子:
2 * i + 2 - arr[i] 父:
int((i-1)/2)
- arr[i] 左孩子:
-
大根堆和小根堆
- 大根堆max-heap:父节点的值大于或等于子节点的值
- 小根堆min-heap: 父节点的值小于或等于子节点的值
-
heap insert for max-heap 时间复杂: O(log N)
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: Pop the root of a max-heap, then move the last node to the top and rearrange the tree from top-to-bottom to be max-heap again. -- 时间复杂O(log N)
public static void heapify(int[] arr, int index, int heapsize){
int left = index * 2 + 1;//左孩子下标
while(left < heapsize){//下方还有孩子
//左右两个孩子,把值大的下表给largest
int largest = left + 1 < heapsize && arr[left + 1] > arr[left] ?
left + 1 : left;
//较大的孩子和父亲比,把值大的下表给largest
int largest = arr[largest] > arr[index] ? largest : index;
if (largest == index){
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
1.2.6.2 例题
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。 解题思路:选定前k个数,做小根堆排序,第一个数一定最小,pop第一个,加入K+1的数继续小根堆排序,直到最后。时间复杂度O(N log K)
priorityQueue<Integer> heap = new Priority Queue<>();// min-heap in java
注意⚠️:在java中,优先级结构就是小根堆,可以自动由小到大排序。可以将他看作黑盒,作用就是排序,并返回最小值。但不能高效地改变堆中的值(遍历),所以有时需要手写堆(heap insert, heapify),以便高效查询和修改
public void sortedArrDistanceLessK(int[] arr, int k){
//默认min-heap
priorityQueue<Integer> heap = new Priority Queue<>();
for(int index = 0; index <= Math.min(arr.length, k); index++){
heap.add(arr[index]);
}
for(int i = 0; index < arr,length; index++){
heap.add(arr[index]);
arr[i] = heap.poll();
}
while(!=heap.isEmpty()){
arr[i++] = heap.poll();
}
}
1.2.7 桶排序 bucket
不基于比较的排序(计数排序)--需要根据数据状况定制
1.2.8 基数排序radix sort
不重要
1.3 排序算法的稳定性及汇总
- 稳定性:值相同的元素保持排序完成后相对次序不变
- 稳定:冒泡/插入/归并/桶/基数
- 不稳定:选择/快速/堆
- 总结:
| 时间复杂 | 空间复杂 | 稳定性 | |
|---|---|---|---|
| 选择 | O() | O(1) | No |
| 冒泡 | O() | O(1) | Yes |
| 插入 | O() | O(1) | Yes |
| 归并 | O(N logN) | O(N) | Yes |
| 快排 | O(N logN) | O(logN) | No |
| 堆 | O(N logN) | O(1) | No |
-
一般选快排,有空间限制选堆,有稳定性要求选归并
-
基于比较的排序,时间复杂度无法低于O(N logN)
-
时间复杂度O(N logN),空间复杂度不能小于O(N)且稳定
-
坑⚠️:
- 归并可以把空间复杂度降为O(1)-->"归并排序 内部缓存法",但是很难且不稳定。不如堆
- "原地归并排序",时间复杂升为O()
- “01 stable sort”,稳定快排但很难,无意义
- 所有的改进都不重要
- 题目:奇数放数组左边,偶数放数组右边,还要原始的相对次序不变,时间复杂O(),空间复杂(1)--> 跟快排评判标准一样(0,1标准),但做不到稳定
- 工程上对排序改进:
- 综合排序:充分利用O(N logN)排得快 和O()空间小的各自优势
- 稳定性
1.4 对数器和比较器
1.4.1 对数器
- 通过用大量测试数据来验证算法是否正确的一种方式
- java中:
Math.random()[0,1)所有小数,等概率返回一个Math.random()*N[0,N)所有小数,等概率返回一个(int)(Math.random()*N)[0,N-1]所有整数,等概率返回一个 - 生成随机array,可以控制长度和最大值
public static int[] generateRandomArray(int maxSize, int maxValue){
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;
}
- for testing
public static void main (String[] args){
int testTime = ;
int maxSize = ;
int maxValue = ;
boolean succeed = true;
for (int i = 0; i < testTime; i++){
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
//method1
//method2
if(!isEqual(arr1,arr2)){
//System.out.println(arr1);
//System.out.println(arr2);
succeed = false;
break;
}
}
System.out.println(succeed? "succeed": "failed")
}
1.4.2 比较器
- 实质是重载比较运算符(自己定义比较方法)
- 应用在特殊标准的排序上
- 应用在根据特殊标准排序的结构上
- 使用方法:
Array.sort(arr, comparator)- 返回负数时,第一参数排前面
- 返回正数时,第二参数排前面
- 返回0 时,谁在前面无所谓
- 大根堆比较器:
public static class Acomp implements Comparator<Integer>{
public int compare(Integer arg0, Integer arg1){
return arg1 - arg0;
}
}
2. LinkedList 链表
2.1 哈希表 Hash table
- 哈希表在使用层面上可以理解为一种集合结构
- 有无value是HashMap和HashSet唯一的区别,底层结构相同
- 哈希表增
hashset.add()删hashset.remove()改hashset.put(key,value)查hashset.contains()/hashset.get(key)时间复杂度都是常数级别O(1) - 放入哈希表的东西,如果是基础类型,内部按值传递(拷贝一份放在哈希表),内存占用就是这个东西的大小
- 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是内存地址,全部8字节
2.2 有序表 Treeset
-
哈希表在使用层面上可以理解为一种集合结构
-
有无value是TreeMap和TreeSet唯一的区别,底层结构相同
-
有序表跟哈希表的区别是,有序表的key按照顺序组织起来,哈希表没有顺序。所以有序表能提供更多操作:
TreeMap.firstKey()最小TreeMap.lastKey()最大TreeMap.floorKey(8)在表中所有<=8 的数中,离8最近的TreeMap.ceilingKey(8)在表中所有>=8 的数中,离8最近的
-
红黑树,AVL树,size-balance-tree都是有序表结构
-
有序表增
treeset.add()删treeset.remove()改treeset.put(key,value)查treeset.contains()/treeset.get(key)时间复杂度都是O(log N) -
放入有序表的东西,如果是基础类型,内部按值传递(拷贝一份放在哈希表),内存占用就是这个东西的大小
-
放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是内存地址,全部8字节
2.3 链表 Linked list
2.3.1 单链表结构 Single Linked list
Class Node<V>{
V value;
Node next;
}
2.3.2 双链表结构 Doubly linked list
Class Node<V>{
V value;
Node next;
Node last;
}
单链表和双链表结构,只需要给一个头节点head,就能找到剩下所有的节点
2.3.3 例1:反转单链表和反转双链表 -- leetcode 206 Reverse Linked List (easy)
换头时要返回Node f(head),换头操作 head = f(head)
时间复杂度O(N), 空间复杂度O(1)
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
temp = cur.next;// 先保存下一个节点
cur.next = prev;// 反转
// 更新prev、cur位置
// prev = cur;
// cur = temp;
return reverse(cur, temp);
}
}
2.3.4 例2:打印两个有序链表的公共部分
思路:每个链表都有一个指针,谁的数小谁移动,值一样就打印,然后一起移动,直到一个链表越界
public void printCommonPart(Node head1, Node head2){
System.out.print('Common Part:');
while (head != null && head2 != null){
if(head1.value < head2.value){
head1 = head1.next;
}else if (head1.value > head2.value){
head2 = head2.next
}else {
System.out.print(head1.value + " ");
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}
2.3.5 链表方法论--笔试和面试要求不同
- 笔试:只在乎时间复杂度,不在乎空间复杂度
- 面试:时间复杂度最优,且找到空间最省的方法
- 重要技巧:
- 额外数据结构记录(哈希表等)
- 快慢指针
2.3.6 例3 :判断链表是否为回文结构 -- leetcode 234 Palindrome Linked List (easy)
笔试思路:放进栈里,每出来一个和原链表比较是否相同,有一个不一样就不是
public static boolean isPalindrome(Node head){// need n extra space
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while(cur != null){
stack.push(cur);
cur = cur.next;
}
while(head != null){
if(head.value != stack.pop().value){
return false;
}
head = head.next;
}
return true;
}
优化:使用快慢指针,快指针每次走两步,慢指针每次走一步,快指针遍历完成时,慢指针到中点。此时把后半部分放入栈中,与原链表比对。省一半的空间 注:快慢指针有时需要定制,要coding熟悉 面试优化:时间复杂O(N),空间复杂O(1): 使用快慢指针,快指针每次走两步,慢指针每次走一步快指针遍历完成时,慢指针到中点。后半部分遍历时,逆序后半部分链表(中点指向null)。然后从链表的头尾同时向中间走(使用引用记住头和尾),比较值是否相同,走到空,停止。返回T/F,之前把链表还原成最初的样子
2.3.7 例4: 将单链表按值划分为左边小,中间相等,右边大的形式 -- leetcode--86 Partition List medium (medium)
笔试思路:ArratList<Node>[] 在数组中partition
面试思路:时间复杂度O(N),空间复杂度O(1)
用6个额外变量:LowHead, LowTail, EqualHead, EqualTail, HighHead, HighTail 然后用指针指
public ListNode partition(ListNode head, int x) {
ListNode sH = null;//samll head
ListNode sT = null;//samll tail
ListNode eH = null;//equal head
ListNode eT = null;//equal head
ListNode lH = null;//large head
ListNode lT = null;//large tail
ListNode next = null;//save next node
while (head != null){
next = head.next;
head.next = null;
if(head.val < x){
if(sH == null){
sH = head;
sT = head;
}else{
sT.next = head;
sT = head;
}
}else if(head.val == x){
if(eH == null){
eH = head;
eT = head;
}else{
eT.next = head;
eT = head;
}
}else{
if(lH == null){
lH = head;
lT = head;
}else{
lT.next = head;
lT = head;
}
}
head = next;
}
//small and equal reconnect
if (sT != null){//有小于区域
sT.next = eH;
eT = eT == null? sT : eT;//下一步谁去练大区域的头,谁就变eT
}
//上面的if,不管跑了没有,et
//all reconnect
if(eT != null){
eT.next = lH;
}
return sH != null ? sH :(eH !=null ? eH : lH);
}
leetcode--86 Partition List medium (medium) 不需要等于部分,只需要小于和大于等于
2.3.8 例5:复制含有随机指针节点的链表--leetcode 138 Copy List with Random Pointer (medium)
一种特殊的链表节点类描述如下:
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
next指针和正常单链表中next指针的意义 一 样,都指向下一个节点。 rand指针是Node类中新增的指针,这个指针可能指向链表中的任意一个节点,也可能指向null。 给定一个由 Node节点类型组成的无环单链表的头节点head,请实现一个 函数完成 这个链表中所有结构的复制,并返回复制的新链表的头节点。
- 笔试思路:hashmap key(Node 老节点)value(Node 复制出的新结果) 1‘ next指针,通过查询2的map出来的2’,1‘指向2’。1‘的rand指针,通过查询原链表中1的rand指针指向的节点,map到复制链表的节点,1’rand指向该复制节点。
class Solution {
public Node copyRandomList(Node head) {
HashMap<Node,Node> map = new HashMap<Node,Node>();
Node cur = head;
while(cur != null){
map.put(cur,new Node(cur.val));
cur = cur.next;
}
cur = head;
while(cur != null){
//cur 老链表
//map.get(cur)新链表
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
- 面试要求:要求时间复杂度O(N),空间复杂O(1)
思路:不使用hashmap。
- 把复制出来的节点,插入原链表。原链表变成1->1'->2->2'->3->3',每一个节点指向复制出的节点。
- 找rand节点时,复制出的节点rand指向原节点rand指向的下一个,因为原节点rand指向的下一个,就是复制出来的节点
- output时,通过next.next 删除原链表只留下复制出来的链表
class Solution{
public Node copyRandomList(Node head) {
if (head == null){
return null;
}
Node cur = head;
Node next = null;
//copy node and link to every node
//1 ->2
//1-> 1' ->2
while(cur != null){
next = cur.next;
cur.next = new Node(cur.val);//1->1'
cur.next.next = next;//1->1'->2
cur = next;//
}
cur = head;
Node curCopy = null;
//set copy node random
//1-> 1' ->2->2'
while(cur != null){
next = cur.next.next;
curCopy = cur.next;
curCopy.random = cur.random != null ? cur.random.next : null;
//cur.rand.next是copynode的rand
cur = next;
}
Node res = head.next;
cur = head;
//split
while (cur != null){
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res;
}
}
2.3.9 例6:两个单链表相交的一系列问题(最难!)
给两个可能有环也可能没环的单链表,头节点head1和head2。如果两个链表相交,返回相交的第一个节点,如果不相交,返回null --> 空间O(1)
2.3.9.1 HashSet 判断是否有环 -- leetcode 141 Linked List Cycle (easy)
时间O(N),空间O(N)
public class Solution {
public boolean hasCycle(ListNode head) {
HashSet<ListNode> set = new HashSet<ListNode>();
while(head != null){
if(set.contains(head)){
return true;
}
set.add(head);
head = head.next;
}
return false;
}
}
2.3.9.2 快慢指针判断是否有环 -- leetcode 142 Linked List Cycle II (medium)
时间O(N),空间O(1) 思路:快慢指针,慢指针一次走一步,快指针一次走两步。若有环,两个指针一定相遇。相遇时,快指针回到开头,慢指针不动,两个指针此时都变成一次走一步,再次相遇时,一定在入环节点。
public ListNode getLoopNode(ListNode head){
if(head == null || head.next == null || head.next.next == null){
return null;
}
ListNode slow = head.next;
ListNode fast = head.next.next;//两个点的初始条件不能相同,因为下面while的判断条件时相不相同
while (fast != slow){
if(fast.next == null || fast.next.next == null){
return null;
}
fast = fast.next.next;
slow = slow.next;
}
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
2.3.9.3 两个链表都没环,找到相交的第一个节点
若两个链表都没环,如果相交,那么相交部分一直到最后肯定相同。遍历完两个链表,并记录下长度,对比最后一个节点是否相同。不相同没相交,相同就相交。得知相交后,重新遍历两个链表。方法为,长度长的从长度短的地方开始遍历(例,一个长100,一个长80,重新走的时候,100先走20,然后和80一起走),两个链表一起走,直到相交节点。
public static Node noLoop(Node head1, Node head2){
if (head1 == null || head2 == null){
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int n =0;
while (cur1 != null){
n++;
cur1 = cur1.next;
}
while (cur2 != null){
n--;
cur2 = cur2.next;
}
if(cur1 != cur2){
return null;
}
//n是链表1长度-链表2长度
cur1 = n > 0 ? head1 : head2;//把长的变成cur1
cur2 = cur1 == head1 ? head2 : head1;//把短的变成cur2
n = Math.abs(n);
while(n != 0){
n--;
cur1= cur1.next;
}
while(cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
2.3.9.4 两个链表一个有环,另一个没环 --> 一定不可能相交
2.3.9.5 两个链表都有环
- 不相交
- 先相交再入环:
- 分别入环
情况2: 先相交再入环,可以把入环节点作为结束点,跟无loop时的遍历一样,找到入环节点。跟有没有环没关系 情况1和情况3: 把loop1再走一遍,如果能遇到loop2,就是分别入环,遇不到就是不相交。
public static Node bothLoop(Node head1, Node loop1, Noode head2, Node loop2){
Node cur1 = head1;
Node cur2 = hdea2;
if(loop1 == loop2){//情况2:公用一个loop
cur1 = head1;
cur2 = head2;
int = 0;
while (cur1 != loop1){
n++;
cur1 = cur1.next;
}
while (cur2 != loop2){
n--;
cur2 = cur2.next;
}
//n是链表1长度-链表2长度
cur1 = n > 0 ? head1 : head2;//把长的变成cur1
cur2 = cur1 == head1 ? head2 : head1;//把短的变成cur2
n = Math.abs(n);
while(n != 0){
n--;
cur1= cur1.next;
}
while(cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
}else{//没有公用loop
cur1 = loop1.next;
while(cur1 != loop1){
if(cur1 == cur2){
return cur1;//情况3
}
cur1 = cur1.next;
}
return null;//情况1
}
}
2.3.9.6 main函数调用方法
public static Node getIntersectNode(Node head1, Node head2){
if(head1 == null || head2 == null){
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if(loop1 == null && loop2 == null){
return noLoop(head1,head2);
}
if(loop1 != null && loop2 != null){
return bothLoop(head1, loop1, head2, loop2)
}
return null;
}
3. binary tree 二叉树
3.1 二叉树结构
// Definition for a binary tree node.
public class TreeNode {
int val;
TreeNode left;//pointer to left child
TreeNode right;//pointer to right child
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
3.2 二叉树递归遍历
3.2.1 递归序
public static void f(Node head){
//1
if(head == null){
return;
}
//1
f(head.left);
//2
//2
f(head.right);
//3
//3
}
递归时,一个点一定会被调3次,第一次时到,第二次是从左孩子回来,第三次是从右孩子回来。 顺序如下:
3.2.2 先序遍历 -- 根左右
1245367 --> 递归序时,第一次到达此处时print
public static void preOrderRecur(Node head){
//1
if(head == null){
return;
}
//1
System.out.print(head.value + "");
f(head.left);
f(head.right);
}
3.2.3 中序遍历 -- 左根右
4251637 --> 递归序时,第二次到达此处时print
public static void inOrderRecur(Node head){
if(head == null){
return;
}
f(head.left);
//2
System.out.print(head.value + "");
//2
f(head.right);
}
3.2.4 后序遍历 -- 左右根
4526731 --> 递归序时,第三次到达此处时print
public static void posOrderRecur(Node head){
if(head == null){
return;
}
f(head.left);
f(head.right);
//3
System.out.print(head.value + "");
//3
}
3.3 二叉树非递归遍历(任何递归都可以改成非递归)
3.3.1 非递归先序遍历 -- 根左右
- 首先把root放入栈中
- 从栈中弹出一个节点cur
- 打印cur
- 把该点的孩子,先右后左放入(如果有),没有就什么也不做
- 重复上述步骤
public static void PreOrderUnRecur(Node head){
if (head != null){
Stack<Node> stack = new Stack<Node>();
stack.add(head);
while(!stack.isEmpty()){
head = stack.pop();
System.out.println(head.val+" ");
if(head.right != null){
stack.push(head.right);
}
if(head.left != null){
statck.push(head.left);
}
}
}
}
3.3.2 非递归后序遍历 -- 左右根
- 首先把root放入栈中
- 从栈中弹出一个节点cur
- 把cur放入辅助栈中
- 把该点的孩子,先左后右放入(如果有),没有就什么也不做
- 重复上述步骤,直到最后
- 打印辅助栈
public static void posOrderUnRecur(Node head){
if (head != null){
Stack<Node> stack1 = new Stack<Node>();
Stack<Node> stack2 = new Stack<Node>();
stack1.push(head);
while(!stack1.isEmpty()){
head = stack1.pop();
stack2.push(head);
if(head.left != null){
stack1.push(head.right);
}
if(head.right != null){
statck1.push(head.left);
}
}
while(!stack2.isEmpty()){
System.out.print(stack2.pop().val + " ");
}
}
}
3.3.3 非递归中序遍历 -- 左根右
- 整个树的左边界依次入栈
- 依次弹出,弹出时打印
- 若弹出节点有右树重复以上步骤(右树的整个左边界一次入栈)
- 打印栈
public static void inOrderUnRecur(Node head){
if(head != null){
Stack<Node> stack = new Stack<Node>();
while(!stack.isEmpty() || head != null){
if(head !=null){
stack.push(head);
head = head.left;//把左边界全部入栈
}else{//head变空了,左边界全部入栈
head = stack.pop();//弹出节点
System.out.print(head.val + " ");
head = head.right;//节点变成右孩子,此时head如果有左孩子,就非空,继续把左边界压入栈
}
}
}
}
3.4 二叉树广度优先遍历BFS
- 使用queue队列(先进先出)
- 先放入root
- 从queue中弹出一个节点
- 打印,并先放入左孩子,再放入右孩子
- 循环
public static void bfs(Node head){
if(head == null){
return;
}
Queue<Node> queue = new LinkedList<Node>();
queue.add(head);
while(!queue.isEmpty){
Node cur = queue.poll();
System.out.print(cur.val + " ");
if(cur.left != null){
queue.add(cur.left);
}
if(cur.right != null){
queue.add(cur.right);
}
}
}
例题:求一颗二叉树宽度 -- leetcode 662 Maximum Width of Binary Tree (medium)
3.5 二叉树相关概念及实现判断
3.5.1 判断是否是搜索二叉树 -- 98 Validate Binary Search Tree (medium)
A valid BST is defined as follows:
- The left subtree of a node contains only nodes with keys less than the node's key.
- The right subtree of a node contains only nodes with keys greater than the node's key.
- Both the left and right subtrees must also be binary search trees.
思路:中序遍历,搜索二叉树一定升序
class Solution{
public boolean isValidBST(TreeNode head){
List<TreeNode> inOrderList = new ArrayList<>();
process(head, inOrderList);
for (int i = 0; i < inOrderList.size() -1; i++){
if(inOrderList.get(i).val >= inOrderList.get(i+1).val){
return false;
}
}
return true;
}
public void process(TreeNode head, List<TreeNode> inOrderList){
if (head == null){
return;
}
process(head.left, inOrderList);
inOrderList.add(head);
process(head.right, inOrderList);
}
}
套路思路:需要左右树信息:1. 是否是搜索二叉树 (左max +右min)--> 2.max 3. min
class Solution{
public boolean isValidBST(TreeNode head){
if(head == null){
return true;
}
return process(head).isBST;
}
public class Info{
public boolean isBST;
public int max;
public int min;
public Info(boolean iss, int ma, int mi){//constructor
isBST = iss;
max = ma;
min = mi;
}
}
public Info process(TreeNode x){
if(x == null){
return null;
}
Info leftData = process(x.left);
Info rightData = process(x.right);
int min = x.val;
int max = x.val;
if(leftData != null){
min = Math.min(min, leftData.min);
max = Math.max(max, leftData.max);
}
if(rightData != null){
min = Math.min(min, rightData.min);
max = Math.max(max, rightData.max);
}
boolean isBST = true;
if(leftData != null && (!leftData.isBST || leftData.max >= x.val)){
isBST = false;
}
if(rightData != null && (!rightData.isBST || x.val >= rightData.min)) {
isBST = false;
}
return new Info(isBST, max, min);
}
}
原因:递归是对每个点的要求应该相同
3.5.2 判断是否是完全二叉树 958 Check Completeness of a Binary Tree --(medium)
In a complete binary tree, every level, except possibly the last, is completely filled, and all nodes in the last level are as far left as possible. 除了最后一层都是满的,且最后一层在从左到右正在变满
思路:层序遍历
- 任意节点,只有右孩子没有左孩子--false
- 在1不违规是,遇到第一个左右孩子不双全的情况,接下来遇到的所有节点都必须是叶子
- 如果符合12,就是完全二叉树
class Solution {
public boolean isCompleteTree(TreeNode head) {
if (head == null){
return true;
}
Queue<TreeNode> queue = new LinkedList<>();
boolean leaf = false;//判断是否遇到第一个左右孩子不双全的节点,遇到前都是false,遇到后都是true
TreeNode l = null;
TreeNode r = null;
queue.add(head);
while(!queue.isEmpty()){
head = queue.poll();
l = head.left;
r = head.right;
if((leaf && (l != null || r != null))||(l == null && r != null)){
//条件2 || 条件1
//已经遇到了不双全的叶节点,但当前节点竟然后孩子 || 只有右孩子没有左孩子
return false;
}
if(l != null){
queue.add(l);
}
if(r != null){
queue.add(r);
}
if(l == null || r == null){//可能会被多次标记,但只要标记过后就是true
leaf = true;
}
}
return true;
}
}
3.5.3 判断是否为满二叉树--套路解题
除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 套路分析:需要左右树的信息:1.深度,2.节点个数
class Solution{
public boolean isFull(TreeNode head){
if(head == null){
return true;
}
Info data = process(head);
return data.nodes == ((1 << data.height) - 1);//深度为k 的满二叉树必有2^k-1 个节点
}
public class Info{
public int height;
public int nodes;
public Info(int h, int n){//constructor
height = h;
nodes = n;
}
}
public Info process(TreeNode x){
if(x == null){
return new Info(0,0);
}
Info leftData = process(x.left);
Info rightData = process(x.right);
int height = Math.max(leftData.height, rightData.height) + 1;
int nodes = leftData.nodes + rightData.nodes + 1;
return new Info(height, nodes);
}
}
3.5.4判断是否为平衡二叉树--110 Balanced Binary Tree (easy)
它的左子树和右子树的高度之差(平衡因子)的绝对值不超过1且它的左子树和右子树都是一颗平衡二叉树。 套路分析:需要左右子树的信息:1.是否是平衡二叉树,2.高度
class Solution{
public boolean isBalanced(TreeNode head){
if(head == null){
return true;
}
return process(head).isBalanced;
}
public class Info{
public int height;
public boolean isBalanced;
public Info(int h, boolean isb){//constructor
height = h;
isBalanced = isb;
}
}
public Info process(TreeNode x){
if(x == null){
return new Info(0, true);
}
Info leftData = process(x.left);
Info rightData = process(x.right);
int height = Math.max(leftData.height, rightData.height) + 1;
boolean isBalanced = leftData.isBalanced && rightData.isBalanced && Math.abs(leftData.height - rightData.height) < 2;
return new Info(height, isBalanced);
}
}
3.5.5二叉树套路 -- 解决树形DP问题
思路:返回左树信息+右树信息并整合,且继续递归
3.6 最低公共祖先--236 Lowest Common Ancestor of a Binary Tree (medium)
Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
According to the definition of LCA : “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”
思路:用HashMap储存<节点,父节点>,递归遍历所有点,都记下来。然后对点1,用hashset把他所有的祖先都找出来,一直找到head。然后对点2,也是一直找,并判断此时的祖先在不在点1 的set中
class Solution{
public TreeNode lowestCommonAncestor(TreeNode head, TreeNode o1, TreeNode o2){
HashMap<TreeNode, TreeNode> fatherMap = new HashMap<>();//记录这个点和这个点的父节点
fatherMap.put(head,head);//traverse方法无法放入head的父,所以手动规定
traverse(head, fatherMap);
HashSet<TreeNode> set1 = new HashSet<>();
set1.add(o1);
TreeNode cur = o1;
while(cur != fatherMap.get(cur)){//只有head可以等于自己的父
//不是父时,往上找
set1.add(cur);
cur = fatherMap.get(cur);
}
set1.add(head);//最后加入head
HashSet<TreeNode> set2 = new HashSet<>();
TreeNode curt = o2;
while(curt != fatherMap.get(curt)){
set2.add(curt);
if(set1.contains(curt)){
return curt;
}
curt = fatherMap.get(curt);
}
return head;
}
public void traverse(TreeNode head, HashMap<TreeNode, TreeNode> fatherMap){
if(head == null){
return;
}
fatherMap.put(head.left, head);
fatherMap.put(head.right, head);
traverse(head.left, fatherMap);
traverse(head.right, fatherMap);
}
}
非常厉害的答案: 两种情况:
- 点1是点2的最低公共祖先,或点2是点1的最低公共祖先
- 点1和点2 不互为公共祖先,只能向上找才能找到最低公共祖先 下面的代码两种情况都对,但并不是我能想出来的
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q){
return root;
}
TreeNode left = lowestCommonAncestor (root.left, p, q);
TreeNode right = lowestCommonAncestor (root.right, p, q);
if(left != null && right != null){
return root;
}
return left != null ? left : right;
}
}
3.7 二叉树中找到一个节点的后继节点 -- 510 Inorder Successor in BST II(medium)
Given a node in a binary search tree, return the in-order successor of that node in the BST. If that node has no in-order successor, return null. The successor of a node is the node with the smallest key greater than node.val. You will have direct access to the node but not to the root of the tree. Each node will have a reference to its parent node. Below is the definition for Node:
class Node {
public int val;
public Node left;
public Node right;
public Node parent;
}
后继节点:中序遍历排序后,本节点的下一个节点 (前驱节点:中序遍历排序后,本节点的上一个节点) 时间复杂度需要从O(N) 优化成O(K)K是两个点的真实距离 思路: 情况1: node有右树时,node后继节点是右树上的最左节点 情况2: node无右树时,往上找,一直找到一个点是其父节点的左孩子,此时node的后继节点就是找到点的父节点。若一直找不到,说明这个点是最右的点,return null
class Solution {
public Node inorderSuccessor(Node node) {
if(node == null){
return node;
}
if(node.right != null){//情况1, 有右子树
return getMostLeft(node.right);
}else{//情况2:无右子树
Node parent = node.parent;
while(parent != null && parent.left != node){//当前节点有父节点,且当前节点是父节点的右孩子
node = parent;
parent = node.parent;//往上走
}
return parent;//当此节点为左节点,返回他的父节点。当遍历完成,到root时也不是左孩子,此时parent就是null,所以返回null
}
}
public Node getMostLeft(Node node){
if(node == null){
return node;
}
while(node.left != null){//要判定有没有左孩子
node = node.left;
}
return node;
}
}
3.8 二叉树序列化和反序列化 -- 297 Serialize and Deserialize Binary Tree (hard)
Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.
Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure.
Clarification: The input/output format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.
例:先序遍历树,下划线_表示值的结束,#表示null
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode head) {
if(head == null){
return "#_";
}
String res = head.val + "_";
res += serialize(head.left);
res += serialize(head.right);
return res;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String[] values = data.split("_");
Queue<String> queue = new LinkedList<String>();
for(int i = 0; i != values.length; i++){
queue.add(values[I]);
}
return reconnect(queue);
}
public TreeNode reconnect(Queue<String> queue){
String value = queue.poll();
if(value.equals("#")){
return null;
}
TreeNode head = new TreeNode(Integer.valueOf(value));
head.left = reconnect(queue);
head.right = reconnect(queue);
return head;
}
}
3.9 折纸问题
请把纸条竖着放在桌⼦上,然后从纸条的下边向上⽅对折,压出折痕后再展开。此时有1条折痕,突起的⽅向指向纸条的背⾯,这条折痕叫做“下”折痕 ;突起的⽅向指向纸条正⾯的折痕叫做“上”折痕。如果每次都从下边向上⽅ 对折,对折N次。请从上到下计算出所有折痕的⽅向。给定折的次数n,请返回从上到下的折痕的数组,若为下折痕则对应元素为"down",若为上折痕则为"up"。
每一个左子树的头节点都是凹,右子树的头节点都是凸
最优解如下:空间复杂度O(N)
public void printAllFolds(int N){
//主函数是1,凹
printProcess(1, N, true);
}
//递归过程,来到某一节点
//i是节点层数,N是一共几层,down== true 凹,down == false 凸
public void printProcess(int i, int N, boolean down){
if(i > N){//超过层数,return
return;
}
printProcess(i + 1, N, true);
//中序遍历
System.out.println(down ? "凹" : "凸");
printProcess(i + 1, N, false);
}
4. Graph 图
4.1 图的基本信息
- 图的存储方式
- 邻接表
- 邻接矩阵
- 使用自己熟悉的图的表示方法,表示图,遇到题是就把图转成自己的表示方法,直接调模版。 模版:
public class Graph{
public HashMap<Interger, Node> nodes;//点集:编号,点的信息
public HashSet<Edge> edges;//边集
public Graph(){
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
public class Node{
public int value;//or string 点上的值
public int in;//点的入度,有多少边进入自己的这个点
public int out;//点的出度,从这个点出去几个边
public ArrayList<Node> nexts;//从这个点发现出去的边,有几个边相连
public ArrayList<Edge> edges;//有几条边属于这个点
public Node(int value){
this.value = value;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
public class Edge{
public int weight;//距离
public Node from;
public Node to;
public Edge(int weight, Node from, Node to){
this.weight = weight;
this.from = from;
this.to = to;
}
}
public Graph createGraph(Integer[][] matrix){
Graph graph = new Graph();
for(int i = 0; i < matrix,length; i++){
Integer from = matrix[i][0];
Integer to = matrix[i][1];
Integer weight = matrix[i][2];
if(!graph.nodes.containsKey(from)){//看这个点有没有出现过
graph.nodes.put(from, new Node(from));//没出现过就新建
}
if(!graph.nodes.containsKey(to)){//看这个点有没有出现过
graph.nodes.put(to, new Node(to));//没出现过就新建
}
Node fromNode = graph.nodes.get(from);
Node toNode = graph.nodes.get(to);
//拿出来两个点建边
Edge newEdge = new Edge(weight, fromNode, toNode);
fromNode.nexts.add(toNode);//这个边是由from到to,所以在from的邻居里加入to
fromNode.out++;//出度++
toNode.in++//入度++
fromNode.edges.add(newEdge);//把边加入from点的数据中
graph.edges.add(newEdge);//把边加入edge的数据中
}
}
4.2 图的遍历
4.2.1 宽度优先遍历 BFS
- 利用queue实现
- 从源节点开始依次放入queue中,然后弹出
- 每弹出一个点,把该节点的所有没有进过queue的邻接点放入queue
- 直到queue空
//从node出发,BFS
//不需要边
public void bfs(Node node){
if(node == null){
return;
}
Queue<Node> queue = new LinkedList<>();
HashSet<Node> set = nre HashSet<>();//确保点不会重复遍历,如果经历过,set中一定有记录
queue.add(node);
set.add(node);
while(queue.isEmpty()){
Node cur = queue.poll();
System.out.println(cur.value);//具体操作具体分析
for(Node next : cur.nexts){
if(!set.contains(next)){
set.add(next);
queue.add(next);
}
}
}
}
4.2.2 深度优先遍历 DFS
- 利用stack实现
- 从源节点开始依次放入stack中,然后弹出
- 每弹出一个点,把该节点的下一个没有进过stack的邻接点放入stack
- 直到stack空
public void dfs(Node node){
if(node == null){
return;
}
Stack<Node> stack = new Stack<>();
HashSet<Node> set = new HashSet<>();
stack.add(node);
set.add(node);
System.out.println(node.value);//加入时处理,具体操作具体分析
while(!stack.isEmpty()){
Node cur = stack.pop();
for(Node next : cur.nexts){
if(!set.contains(next)){
stack.push(cur);//先把本点压入栈
stack.push(next);//再把下一个点压入栈
set.add(next);//set加入邻居
System.out.println(next.value);//处理邻居,具体操作具体分析
break;//不看其他可能,再重新开始while循环,此时从邻居点开始下一次循环
}
}
}
}
4.3 拓扑排序算法
使用范围:有向图,且有入度为0的节点,且没有环 找到入度为零的点,记录值,并把这个点及其影响全抹去,找下一个入度为零的点
public List<Node> sortedTopology(Graph graph){
//key 某一个node
//value:剩余的入度
HashMap<Node, Integer> inMap = new HashMap<>();
//入度为0的点,才能进队列
Queue<Node> zeroInQueue = new LinkedList<>();
for(Node node: graph.nodes.values()){
inMap.put(node, node.in);//记录每个点原始入度
if(node.in == 0){
zeroInQueue.add(node);//找到入度为0的点
}
}
//拓扑排序的结果,依次加入result
List<Node> result = new ArrayList<>();
while(!zeroInQueue.isEmpty()){
Node cur = zeroInQueue.poll();
result.add(cur);
for(Node next : cur.next){
inMap.put(next, inMap.get(next) - 1);//抹去此点的影响
if(inMap.get(next) == 0){
zeroInQueue.add(next);
}
}
}
return result;
}
4.4 Kruskal 算法和prim算法
4.4.1 要求无向图,生成最小生成树
所谓一个带权图的最小生成树,就是原图中边的权值最小的生成树 ,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。(能链接所有点且所有边长加起来最短)
4.4.2 Kruskal
克鲁斯卡尔算法(Kruskal)是一种使用贪婪方法的最小生成树算法。 该算法初始将图视为森林,图中的每一个顶点视为一棵单独的树。 一棵树只与它的邻接顶点中权值最小且不违反最小生成树属性(不构成环)的树之间建立连边。(把所有边由小到大排序依次添加,如果形成环则不添加) 代码逻辑:把每个点都单独放入集合,每连一个边,就把两个点放入同一个集合。再连下一条边,若不在一个集合中,就加入,若在一个集合中,就跳过。使用并查集为最优解。因为有可能存在局部联通后整体联通(片和片相连)。所以必须用集合
4.4.3 Prim
代码逻辑:先选点,然后选择所有与点相连的边中最小的,选中下一个点,然后继续选择所有与选中点相连的边中最小的,知道所有点都被链接。不需要集合,只需要hashtable,因为是一个一个点的加入
4.5 Dijkstra算法
单元最短路径算法 要求:没有权值为负的边--(可以有值为负的边,但是不能有相加和为负数的环) 代码逻辑: 1、Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离,和一个保存已经找到了最短路径的顶点的集合:T, 初始时,原点s 的路径权重被赋为 0 (dis[s] = 0)。 若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s,m), 同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。 初始时,集合T只有顶点s。 2、然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点, 3、然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。 4、然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。
5. Trie 前缀树
5.1 前缀树定义
在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值
5.2 前缀树创建
思路:
- 创建TireNode类型,定义pass和end,并创建26条新的空路
- 遍历时,先看有没有这条路径,若有就pass++,没有就创建新的
- 每次字符经过一个点时,pass++,到达结尾点时end++
- 查询前缀是,用pass。查询完整字符串时,用end
public class Trie TrieNode{
public int pass;//经过这个点,pass++
public int end;// 以这个点结尾, end++
public TrieNode[] nexts;// HashMap<Char, Node> nexts;
// or TreeMap<Char, Node> nexts;字符特别多的时候,用哈希表
public TrieNode(){
pass = 0;
end = 0;
//next[0] == null 没有走向“a”的路
//next[25] != null 有走向“z”的路
//提前建好26条路,但后面的节点都是空的
nexts = new TrieNode[26]
}
}
public class Trie{
Trie root;//头节点
public Trie{
root = new TrieNode();
}
public void insert(String word){
if(word == null){
return;
}
char[] chs = word.toCharArray();//把word转换成字符类型的数组
TireNode node = root;//node在根节点出发
node.pass++;//根节点p值:加入了多少个字符串或有多少个字符串是空串为前缀
int index = 0;
for (int i = 0; i < chs.length; i++){//从左到右遍历字符
index = chs[i] - 'a';//每个字母减a的ascii码,这样a的index是0,b是1,c是2
if (node.next[index] == null){
node.next[index] = new TrieNode();
}
node = node.nexts[index];
node.pass++;
}
node.end++;
}
}
5.3 查询word之前加入过几次
public int search(String word){
if(word == null){
return 0;
}
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0, i < chs.length, i++){
index = chs[i] - 'a';
if(node.next[index] == null){
return 0;//树中的节点已经到头,但word还没查询完,说明原来没有加入过
}
node = node.next[index];
}
return node.end;//查询完成,全部匹配,end的值就是加入过几次
}
5.4 所有加入字符串中,有几个是以pre为前缀的
public int prefixNumber(String pre){
if(pre == null){
return 0;
}
char[] chs = pre.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0, i < chs.length, i++){
index = chs[i] - 'a';
if(node.next[index] == null){
return 0;//树中的节点已经到头,但word还没查询完,说明原来没有加入过
}
node = node.next[index];
}
return node.pass;//查询完成,pre 匹配,看pass有几次
5.5 删除
public void delete(String word){//沿途pass--,最后end--
if(search(word) != 0){//先查询树中有没有加入过此word
char[] chs = word.toCharArray;
TrieNode node = root;
node.pass--;
int index = 0;
for (int i = 0; i < chas.length; i++){
index = chs[i] - 'a';
if(--node.nexts[index].pass == 0){//此处pass减完后为0,不需要遍历后续节点,直接把后面全部删除
//java 中可以,jvm直接把null后面的空间自动释放
//c++ 要遍历
node.nexts[index] = null;
return;
}
node = node.nexts[index];
}
node.end--;
}
}
6. greedy 贪心
6.1 贪心算法定义
在某一标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终的道一个答案的算法。也就是说,不从整体最优考虑,所做出的是在某种意义上的局部最优解
局部最优 -> 整体最优
6.2 贪心解题思路
- 实现一个不依靠贪心的解法X,可以用最暴力的尝试
- 脑补出贪心策略A,B,C
- 用解法X和对数器,验证每一个贪心,用实验的方式得知哪个贪心是正确的
- 不纠结贪心策略的证明
6.3 贪心常用技巧
- 根据某标准建立一个比较器来排序
- 根据某标准建立一个比较器来组成堆
6.4 会议室安排会议
一段时间内,能安排的最多会议 思路:按结束时间安排,先安排结束时间早的,然后删除冲突的,剩下的继续找结束时间早的
public static int bestArrange(Program[] programs, int timePoint) {
Arrays.sort(programs, new ProgramComparator());
int result = 0;
// 依次遍历每一个会议,结束时间早的会议先遍历
for (int i = 0; i < programs.length; i++) {
if (timePoint <= programs[i].start) {
result++;
timePoint = programs[i].end;
}
}
return result;
}
public static class ProgramComparator implements Comparator<Program> {
@Override
public int compare(Program o1, Program o2) {
return o1.end - o2.end;
}
}
6.5 切金条
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金 条,怎么分最省铜板? 例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60. 金条要分成10,20,30三个部分。 如果, 先把长度60的金条分成10和50,花费60 再把长度50的金条分成20和30,花费50 一共花费110铜板。 但是如果, 先把长度60的金条分成30和30,花费60 再把长度30金条分成10和20,花费30 一共花费90铜板。 输入一个数组,返回分割的最小代价.
思路:哈夫曼编码(Huffman Coding)
- 把数组放入小根堆
- 选出最小的两个数,结合,把结合后的数重新放入小根堆
- 重复步骤2,直到小根堆空
public int lessMonry(int[] arr){
PriorityQueue<Integer> pQ = new PriorityQueue<>();
for(int i =0; i < arr.length; i++){
pQ.add(arr[i]);
}
int sum = 0;
int cur = 0;
while(pQ.size() > 1){
cur = pQ.poll() + pQ.poll();
sum += cur;
pQ.add(cur);
}
return sum;
}
6.6 最大钱数IPO -- 502 IPO(hard)
思路: 堆!
- 把所有项目按花费放进小根堆(利润无所谓),called 未解锁
- 按初始资金,弹出花费<初始资金的所有项目,按利润放入大根堆called 解锁。
- 按贪心,选利润最大的
- 重复2-3,直到达到项目上限
class Solution {
public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator());
PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
//所有项目都扔进被锁池中,按花费扔小根堆
for(int i = 0; i < profits.length; i++){
minCostQ.add(new Node(profits[i], capital[i]));
}
for(int i = 0; i < k; i++){//进行k轮
//资金够的项目全解锁
while(!minCostQ.isEmpty() && minCostQ.peek().c <= w){
maxProfitQ.add(minCostQ.poll());//按利润放大根堆
}
if (maxProfitQ.isEmpty()){//资金不够解锁项目
return w;
}
w += maxProfitQ.poll().p;
}
return w;
}
public class Node{
public int p;
public int c;
public Node(int p, int c){
this.p = p;
this.c = c;
}
}
public class MinCostComparator implements Comparator<Node>{
@Override
public int compare(Node o1, Node o2){
return o1.c - o2.c;
}
}
public class MaxProfitComparator implements Comparator<Node>{
@Override
public int compare(Node o1, Node o2){
return o2.p - o1.p;
}
}
}
6.7 堆的应用--一个数据流中,随时取得中位数 -- 295 Find Median from Data Stream(hard)
思路:大根堆和小根堆配合 --时间复杂O(log N) 1.first input先进大根堆 2. input 跟大根堆顶比较,input >= 大根堆堆顶,input进小根堆 3. 比较大小根堆的size,如果size相差=2,把数多的堆的堆顶弹出,放入另一个 4.结果:较小1/2在大根堆,较大1/2在小根堆。中位数由两个堆堆顶得出
class MedianFinder {
// To store lower half of data stream eg. 1, 2, 3, 6
PriorityQueue<Integer> lowerHalf;
// To store upper half of data stream eg. 8, 9, 11
PriorityQueue<Integer> upperHalf;
/** initialize your data structure here. */
public MedianFinder() {
lowerHalf = new PriorityQueue<>((a,b) -> b - a); // Max heap : To fetch largest
// element from lower half in O(1) time
upperHalf = new PriorityQueue<>(); // Min heap : To fetch lowest
// element from upper half in O(1) time
}
public void addNum(int num) {
// Insert in lowerHalf is it's empty or if number being inserted is less than the peek of lowerHalf otherwise insert in upperHalf
if(lowerHalf.isEmpty() || num <= lowerHalf.peek()){
lowerHalf.add(num);
}else{
upperHalf.add(num);
}
// We also need to ensure that the halves are balanced i.e. there is no more than a difference of 1 in size of both halves
// Let lowerHalf be the one to hold one extra element if the size of total data stream is odd otherwise be equal to upperHalf
if(upperHalf.size() > lowerHalf.size()){ // If an element added above made upperHalf have one more element than lowerHalf then we poll it and put it into lowerHalf
lowerHalf.add(upperHalf.poll());
} else if(lowerHalf.size() > upperHalf.size() + 1){
// If an element added above, made lowerHalf have 2 more elements then upperHalf then we put one into upperHalf from lowerHalf
upperHalf.add(lowerHalf.poll());
}
}
public double findMedian() {
if(lowerHalf.size() == upperHalf.size()){
return (double)(lowerHalf.peek() + upperHalf.peek())/2;
}else{
return (double)(lowerHalf.peek());
}
}
}
7. 暴力递归
7.1 暴力递归
暴力递归就是尝试
- 把问题转化为规模缩小的同类问题的子问题
- 有明确的不需要继续进行递归的条件 -- base case
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
尝试就是动态规划的基础
7.2 汉诺塔 hanota
打印n层汉诺塔从最左移动到最右的全部过程
思路:设置3个杆子为from,to, other
- 把1到i-1从from移动到other
- 把i 从from 移动到 to
- 把1到i-1 从other移动到to
public void func(int i, String start, String end, String other){
if (i == 1){//base case 只剩最上的圆盘
System.out.println("Move 1 from" + start + "to" + end);
}else{
func(i-1, start, other,end);
System.out.println("move" + i "from" + start + "to" + end);
func(i-1, other, end, start);
}
}
public void hanoi(int n){
if(n > 0){
fun(n, "left", "right", "middle");
}
}
不要考虑全局,考虑局部问题,局部问题限制对了,全局就是对的!
7.3 打印字符串全部子序列,包括空字符串--组合问题-- 78 Subset(medium)
每一个位置都有要跟不要两种选择,穷举所有可能性,就是答案
public void function(int num){
int[] chs = str.toCharArray();
process(chs, 0, new ArrayList<Character>());
}
//当前来到i位置,要和不要走两条路
//res之前的选择,所形成的list
public void process(char[] str, int i, List<Character> res){
if(i == str.length){
printList(res);
return;
}
List<Character> resKeep = copyList(res);//把当前的选择拷贝出来一份新的
resKeep.add(str[i]);//把当前元素加进去,做新的过程
process(str, i+1, resKeep);//要当前字符的路
List<Character> resNoInclude = copyList(res);//把当前的选择拷贝出来一份新的
process(str, i+1, resNoInclude);//不要当前字符的路
}
public void printList(List<Character> res){
//
}
public List<Character> copyList(List<Character> list){
//
}
more efficient
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> output = new ArrayList();
Arrays.sort(nums);
backtrack(output, new ArrayList<>(),nums,0);
return output;
}
public void backtrack(List<List<Integer>> output, ArrayList<Integer> temp,int[] nums, int index){
output.add(new ArrayList<>(temp));
for (int i=index; i< nums.length;i++){
temp.add(nums[i]);
backtrack(output, temp, nums, i+1);
temp.remove(temp.size()-1);
}
}
}
7.4 打印字符串全部序列,不能出现重复序列-- 排列 -- 46 Permutations (medium)
class Solution {
public List<List<Integer>> permute(int[] num) {
List<List<Integer>> results = new ArrayList<List<Integer>>();
Arrays.sort(num);
permute(results,num,0);
return results;
}
public void permute(List<List<Integer>> results, int[] nums, int index){
if(index == nums.length){//base case
ArrayList<Integer> result = new ArrayList<>();
for (int i= 0; i<nums.length; i++){
result.add(nums[i]);
}
results.add(result);
return;
}
for (int i = index; i<nums.length;i++){
swap(nums,index,i);//i 以后所有的位置都能到i位置
permute(results,nums,index+1);//走分支
swap(nums,index,i);//还原成原来的样子
}
}
public void swap(int[] num, int i,int j){
int temp = num[i];
num[i] = num[j];
num[j] = temp;
}
}
7.5 纸牌 -- 486 Predict the Winner (medium)
思路:
class Solution {
public boolean PredictTheWinner(int[] arr) {
if(arr == null || arr.length == 0){
return true;
}
return f(arr, 0, arr.length -1) >= s(arr, 0, arr.length -1);//玩家1先,玩家2后
}
public int f(int[] arr, int i, int j){//先手函数
if(i == j){
return arr[i];//最后一个直接拿走
}
return Math.max(arr[i] + s(arr, i + 1, j), arr[j] +s(arr, i, j-1));//拿i,j后面都要后手
}
public int s(int[] arr, int i, int j){//后手函数
if(i ==j){
return 0;
}
return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
//范围内哪个差,那个是1的,因为后手,好的肯定被对手拿走
}
}
7.6 逆序栈,不申请额外的数据结构,只使用递归
public void reverse(Stack<Integer> stack){
if(stack.isEmpty()){
return;
}
int i = f(stack);
reverse(stack);
stack.push(i);
}
public int f(Stack<Integer> stack){//移除栈底元素并返回
int result = stack.pop();
if(stack.isEmpty()){
return result;
}else{
int last = f(stack);
stack.push(result);
return last;
}
}
7.7 91 Decode Ways(medium)
A message containing letters from A-Z can be encoded into numbers using the following mapping:
'A' -> "1" 'B' -> "2" ... 'Z' -> "26"
To decode an encoded message, all the digits must be grouped then mapped back into letters using the reverse of the mapping above (there may be multiple ways). For example, "11106" can be mapped into:
"AAJF" with the grouping (1 1 10 6) "KJF" with the grouping (11 10 6) Note that the grouping (1 11 06) is invalid because "06" cannot be mapped into 'F' since "6" is different from "06".
Given a string s containing only digits, return the number of ways to decode it.
The test cases are generated so that the answer fits in a 32-bit integer.
思路:从左往右试 只要i位置不是0,就把i位置转成1个字母,i+1后面继续试
- i == 3-9,i只能单独转因为只有26个字母
- i == 1 i和i+1共同组成一个部分,让i+2后面继续试
- i == 2 看和后面一位组成后是否大于26
超时
class Solution {
public int numDecodings(String s) {
if(s == null || s.length() == 0){
return 0;
}
return process(s.toCharArray(), 0);
}
//i之前位置如何转化已经做好决定
//i之后能有多少种转化结果
//记得用单引号
public int process(char[] str, int i){
if(i == str.length){
return 1;
//到达最后位置,返回一种有效的,是之前做的决定。
//因为之前做决定的前提是有效,所以之前所有的决定构成一种结果
}
if(str[i] == '0'){
return 0;//之前做的决定,让现在不能转,因为0没有对应字母,所以return 0,决定无效
}
if(str[i] == '1'){
int res = process(str, i + 1);//i自已单独一部分,看后续有多少种方法
if(i + 1 < str.length){
res += process(str, i + 2);//i后面还有字符,让(i和i+1)作为一部分
}
return res;
}
if(str[i] == '2'){
int res = process(str, i + 1);//i自已单独一部分,看后续有多少种方法
if(i + 1 < str.length && (str[i+1] >= '0' && str[i+1] <= '6')){//要小于等于26
res += process(str, i + 2);
}
return res;
}
//当前字符是3-9,i只能自己转
return process(str, i + 1);
}
}
7.8 背包问题
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少? 思路:从左往右试
public static int knapsackProblem(int[] weights, int[] values, int bag) {
return process(weights, values, bag, 0, 0, 0);
}
public static int process(int[] weights, int[] values, int bag, int alreadyWeight, int alreadyValue, int i) {
// 所有物品尝试完
if (i == weights.length) {
return alreadyValue;
}
// 如果当前袋子超重
if (alreadyWeight > bag) {
return 0;
}
return Math.max(
// 将第i号物品放入袋子中
process(weights, values, bag, alreadyWeight - weights[i], alreadyValue + values[i], i + 1),
// 不将第i号物品放入袋子中
process(weights, values, bag, alreadyWeight, alreadyValue, i + 1)
);
}
7.9 N-queens -- 52 N-queens II (hard)
The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other. Given an integer n, return the number of distinct solutions to the n-queens puzzle.
思路:DFS
class Solution {
public int totalNQueens(int n) {
if(n < 1){
return 0;
}
int[] record = new int[n];// record[i] --> i行的queen,放在了那一列。单数组表示
return process(0, record, n);
}
public int process(int i, int[] record, int n){
//int i --> 目前来到第i行
//record[0..i-1],,之前摆的queens都在record中,表示之前的行,放了皇后的位置
//record[0..i-1] 中的皇后,一定不共行,不共列,不共斜线
//n --> 整体有几行
//return 摆完所有皇后,合理的摆法有多少种
if(i == n){//base case:n是终止行,来到终止行,record中是一种合理的答案
return 1;
}
int res = 0;
for(int j = 0; j < n; j++){//当前行永远在i行,尝试i行所有列 j
//检查会不会和原来的冲突,冲突无效
if(isValid(record, i, j)){
record[i] = j;
res += process(i + 1, record, n);//找下一行
}
}
return res; //统计所有合法的,累加在一起
}
//需要检查record[0..i-1],record[i]不需要,因为i以后没有皇后需要放
//返回i行皇后,放在j列,是否有效
//一定不共行,因为是按行操作的
public boolean isValid(int[] record, int i, int j){
for(int k = 0; k < i; k++){
if(j == record[k] || Math.abs(record[k] - j) == Math.abs(i-k)){
//检查是否共列和是否共斜线
return false;
}
}
return true;
}
}
时间复杂度O(n^n): n行每一行都有n中可能
常数优化版本:用位运算加速。利用位运算,代替record检查