1.
算法复杂度
时间 > 空间 >系数
归并排序
//找中间,递归排序左边,递归排序右边,合并两边 O(N*log(N))
public static void f(int[] arr,int L,int R){
if(L==R)
{
return arr[L];
}
int mid = L + (R-L)>>1;
f(arr,L,mid);
f(arr,mid+1,R);
merge(arr,L,mid,R);
}
public static void merge(int[] arr,int L,int mid,int L){
int[] help = new int[R-L+1];
int i = 0;
int p1 = L;
int p2 = mid+1;
while(p1<=mid&&p2<=R)
{
help[i++] = arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=mid)
{
help[i++] == arr[p1++];
}
while(p2<=R){
help[i++] = arr[p2++];
}
for(i=0;i<help.length;i++)
{
arr[L+i] == help[i];
}
}
-
小数和:
给一个数组arr,针对每一个arr[i],把arr[i]左边的比arr[i]小的数求和,最后得出最终的总和
分析:对于arr[i],可以记录在arr[i]右边的数组中,有几个比arr[i]大的数,即最后的结果要加几个arr[i]
在merge函数中,对于[left,mid],[mid+1,right],合并时,左侧小于右侧时,小和加上当前左侧数字*右侧当前数后面的数子个数 如左侧[1,2,5],右侧[3,4,6],对于左侧的1,右侧的3,左侧的1小于3,所以右侧的3会产生一个1,4会产生一个1,6会产生一个1,即小和中会有 3、4、6这3个1,即1 * 3
-
荷兰国旗
给一个数组arr,一个数num,把数组中小于等于num的放左边,大于num的放右边
时间O(N),空间O(1)
分析:arr[i] 小于num,把小于区边界(初始化为-1)下一个位置与arr[i]交换,小于区边界加一,i++
arr[i]大于num,i++;
给一个数组arr,一个数num,把数组中小于num的放左边,大于num的放右边,等于的放中间
时间O(N),空间O(1)
分析:arr[i] 小于num,把小于区边界(初始化为-1)下一个位置与arr[i]交换,小于区边界加一,i++
arr[i]大于num,把大于区边界(初始化为n)前一个位置与arr[i]交换,大于区边界减一,i不变
arr[i]等于num,i++
快排:
随机选一个数与最后一个替换,然后小于区、大于区做递归,荷兰国旗 空间O(logN)
桶排序
桶结构
大根堆:根节点为最大的完全二叉树,任意子树的根节点也是子树中最大的
保证是一个大根堆:如果当前节点大于父节点,交换两者,直到小于父节点或到根节点
heapinsert : 添加新节点时,如果大于父节点,交换两者,直到小于父节点或到根节点
swap:返回最大值并移除:移除第一个节点,把最后一个节点移到最前面,
heapify : 把左右孩子中的最大值与该节点比较,如果父节点小,交换,直到大于左右孩子最大值或没有左右孩子
PriorityQueue<Integer> heap = new PriorityQueue<>();
heap.add(8);
heap.poll();
冒泡、插补、二分
二分法
//>>右移 除 <<左移 乘
mid = L +((R-L)>>1);//mid = ( L + R )/2;
x = 2*x+1;//x = (x<<1)|1
-
有序数组
- 查找某个数
- 大于等于某个数的最左侧位置
- 小于等于某个数的最右侧位置
-
局部最小值
局部最小
无序数组,相邻不等,返回任意一局部最小
- 判断0处和n处是否为局部最小
- 若两侧都不是局部最小,则中间必有局部最小
- 来到中间,判断是否为局部最小,如果是,直接返回,如果不是,则定有一边比它小,选择比它小的一边,有构成了2的条件,继续找中间
有某个原则可以确定舍弃一边时就可用二分
异或运算
异或运算:二进制无进位相加
0^N = N N^N =0
有交换律和结合律
//交换两个数,a和b的内存不是一个区域,否则会出错,都变成0
a = a ^ b;
b = a ^ b;
a = a ^ b;
常用:
//取数的二进制最右侧的一个1
N&((~N)+1)
//数组中有两个数出现了奇数次,寻找这两个数
- 数组全部异或的 eor
- eor 取最右端的1作为rightOne
- 数组与rightOne 相与,得到该位是1或不是1两种情况
2.二叉树
class Node{
int val;
Node left;
Node right;
}
递归序(重要)
每一个节点都会经历三次
先序、中序、后序都是递归序加工的结果
先序——>递归 栈实现 头 左 右
- 弹出时打印
- 有右,压入
- 有左,压入
头 右 左
- 弹出时打印
- 有左,压入
- 有右,压入
后序:上面反过来即是后序 (左 右 头 )
中序:
- 当前节点下全部左边界压入栈
- 1结束后,弹出一个 打印 压入右节点,进行1
后序遍历:(炫技版)
定义两个变量 h c
h表示刚打印的节点 初始化为head;
c表示当前节点 初始化为null
- 如果h是c的左孩子,表示左孩子已处理完,把c的右孩子入栈
- 如果h是c的右节点,表示右节点已处理完,打印c,更新 h ,把c出栈
- 如果都不是 表示c还没到最后的左子树,把c的左子树入栈
栈先入头节点;
c表示当前节点 初始化为null;
h表示刚打印的一个点 初始化为head;
while(栈不空){
更新c为栈顶节点;
如果h是c的左孩子,表示左孩子已处理完,把c的右孩子入栈;
如果h是c的右节点,表示右节点已处理完,打印c,更新 h ,把c出栈;
如果都不是 表示c还没到最后的左子树,把c的左子树入栈;
}
层序遍历
队列
树的宽度
//队列
queue.add();
Node a = queue.poll();
//根节点入队
//当前节点h
//设置两个变量 当前层、当前层节点数
//设置一个map记录节点所在层数
设置根节点所在层数为1
while(队不空){
h=出队;
如果有左子树,设置左子节点所在的层数,入队;
如果有右子树,设置右子节点所在的层数,入队;
当前节点所在层数==当前层数:
当前层节点数+1;
否则:
//当前层已结束,现在是下一层
更新宽度;
当前层加1;
当前层节点数=1;
}
//最后一层时没有更新宽度
更新宽度
-
不用Map
//设置当前层最右节点,下一层最右节点
序列化和反序列化
-
序列化
子节点为空:加入数组,不加队列
不为空:既加入数组,又加入队列
-
反序列化
3.二叉树进阶
-
打印一棵树的打印函数
-
有父节点,找后继 后继:中序遍历下,一个节点的下一个遍历对象称为其后继
- 有右子树,右子树的最左子节点即为后继
- 没有右子树,向上寻找父节点,判断是否为其左子节点,如果是,该父节点即是后继,如果不是,继续向上,直到找到是其父节点的左孩子的节点
-
纸条的折痕
递归模拟二叉树
// i 当前层数 N 共折N次 down是凹还是凸 true时表示凹 public void f(int i,int N,boolean down){ if(i>N){ return ; } //相当于用递归模拟了二叉树的建立 f(i+1,N,true); System.out.println(down?"凹":"凸"); f(i+1,N,false); }
-
搜索二叉树
//判断是否为搜索二叉树 public static int preValue = Integer.MIN_VALUE; public static boolean f(Node root){ if(root==null) { return true; } //要先判断左子树,因为在判断左子树时会更新preValue boolean b = f(root.left); if(!b) { return false; } if(root.val<= preValue) { return false; } else { preValue = root.Val; } return f(root.right); }
4.二叉树的递归套路
1.给定一个二叉树的头节点,返回这个二叉树是不是平衡树
平衡树:左子树的高度与右子树的高度不超过1 <=1
2.返回最大距离
5.暴力递归到动态规划
1。尝试
子串:连续的 两层循环
子序列:按顺序的
寻找全部的子序列
不重复:设置HashSet
全排列:str[0……i-1]都已决定好,str[i……]都有机会来到i位置,与之交换,这是一个循环过程
不重复的全排列:假设只有26个字符组成字符串,设置一个Hash<Character ,boolean>,
1.从左往右的尝试模型
背包问题
两种递归方式
- 以 int[] w,int[] v,int index,int already,int bag为参数
- 以 int[] w,int[] v,int index,int rest 为参数
2.范围上尝试模型
-
纸牌
//先手在l到r范围上获取的最大分数 public static int f(int[] arr,int l,int r){ if(l==r) { return arr[l]; } //先手有两种挑法,返回最大的那种 return Math.max(arr[l]+s(arr,l+1,r),arr[r]+s(arr,l,r-1)); } public static int s(int[] arr,int l,int r) { if(l==r) { return 0; } else{ // return Math.min(f(arr,l+1,r),f(arr,l,r-1)); } } //返回最终获胜者的分数 return Math.max(f(arr,0,r),s(arr,0,r));
3.N皇后
public static int f(int[] record,int index,int N){
if(index == N)
{
return 1;
}
int res = 0;
for(int j=0;j < N;j++)
{
if(isValid(record,index,j))
{//判断是否共列、共斜线 |a-b| ==|c-d|
record[i] = j;
res = res+f(record,index+1,N);
}
}
}
优化常数项(位运算)
以八皇后为例
//列限制:colLim : 00000...0000| 00000000
//左斜线限制: leftLim 00000...0000| 00000000
//右斜线限制:rightLim 00000...0000| 00000000
//最右边有8个0,表示可以放皇后
limit = 000...000 | 11111111;
public static int f(int limit,int colLim,int leftLim,int rightLim){
if(colLim==limit)
{
return 1;
}
//去除8个0之前的0的干扰
//后八位中为0不可放,为1可放
//某行可选的位置就是lim后8位中为1的位置
lim = limit & (~(colLim | leftLim | rightLim));
//依次提取最右侧的1 a & ( ~a + 1 )
while(lim!=0){//如果中间某行不能放皇后,lim会等于0
mostRIght = lim & (~lim + 1);
mostRIght = lim & (~lim + 1);
res = res + f(limit,colLim|mostRight,(leftLim|mostRight)<<1,(rightLim|mostRight)>>>1)
}
return res;
}
4.记忆化搜索
在递归时加上dp数组,到达边界情况时先记录dp,
暴力递归不一定转成动态规划,但动态规划一定来自于暴力递归
机器人走路
先做暴力递归,不用想转移方程,要找出某种选择下返回之间的联系,据此找出转移方程
5.记忆化搜索与动态规划的差别
递归函数中没有枚举行为(即没有循环)时记忆化搜索与动态规划没区别
而在有枚举行为时,动态规划可以省去记忆化搜索的枚举行为
6单链表
快慢指针:快指针一步走两个,慢指针一步走一个
环的入口处
快指针一次走两步,慢指针一次走一步,肯定会在环上相遇,相遇后,慢指针继续一次走一步,快指针回到头节点,变成一次走一步,再相遇时必定在环入口处
寻找两链表相交处
-
两链表都没有环
-
遍历两链表,记录长度A、B与最后一个节点
-
两个最后一个节点 不是同一个 返回,无交点
是同一个,长的那个先走 |A-B|步,然后两个肯定会同时走到交点处 (a.next == b.next)
-
-
一个有环,一个无环:必不相交
-
都有环 环入口处分别为loop1 、loop2
-
loop1==loop2,与无环相同
-
loop1 != loop2 : 遍历loop1环的过程中没有遇到 loop2,不相交
遇到了,返回loop1或loop2都可以
-