面试 leecode
面试 leecode
一、idea leecode插件安装及配置
1. 安装
IDEA插件安装套路
2. 配置
原配置
//code fileName
[$!{question.frontendQuestionId}]${question.title}
//code template
${question.content}
${question.code}
//templact constant
${question.title} 题目标题 示例:两数之和
${question.titleSlug} 题目标记 示例:two-sum
${question.frontendQuestionId} 题目编号
${question.content} 题目描述
${question.code} 题目代码
$!velocityTool.camelCaseName(str) 转换字符为大驼峰样式(开头字母大写)
$!velocityTool.smallCamelCaseName(str) 转换字符为小驼峰样式(开头字母小写)
$!velocityTool.snakeCaseName(str) 转换字符为蛇形样式
$!velocityTool.leftPadZeros(str,n) 在字符串的左边填充0,使字符串的长度至少为n
$!velocityTool.date() 获取当前时间
改成如下:
- //code fileName
- $!velocityTool.camelCaseName(${question.titleSlug})
- //code template
- ${question.content}
- public class $!velocityTool.camelCaseName(${question.titleSlug}){
- public static void main(String[] args) {
- Solution solution = new $!velocityTool.camelCaseName(${question.titleSlug})().new Solution();
-
- }
- ${question.code}
- }
3. 问题
3-1. 题目中文乱码
idea安装文件vmoptions里增加-Dfile.encoding=utf-8
二、B占左神7天刷题
1. 基础班
1-1. 面试技巧(P1)
1-1-1. 刷题5大问题
- 书看不懂,吃灰
- 简单题过了,中难题写不出来,题看不明白
- 问题问题关联上某个算法和结构,搜到帖子,看不懂,想起了书(算法导论)
- 中等题也能写出来,难的算法题,还是门都没找到
- 不学不看算法,不做算法的工作,某个特定业务,某个算法比你的代码性能提高数倍。
- 帖子难懂
- 书难懂
- 知识体系不足
1-2. 复杂度和简单排序算法(P2)
1-2-1. 复杂度
1-2-1-1. 时间复杂度
- 常数时间复杂度:如果和数据量没关系,叫做常数操作就是流程中的行为节点(int a=arr[i])
- 时间复杂度是算法流程中,常数操作数量的指标,发生多少常数操作
- 常数操作数量的表达式(int b=list.get(i) )只要高阶项,不要系数,然后就是时间复杂度O(f(n))
- 算法流程好坏先看时间复杂度,再看不同数据样本下的常数项时间:不同结构执行时间不同。比如找数数组要比list快。
1-2-1-2. 简单排序算法
- 常数操作(流程中)
- 找数
- 比较大小
- 交换
- 常数操作次数(常数操作数量)
- 找数N+N-1+N-2+...
- 比较N+N-1+N-2+...
- swap:N次
一共常数操作次数表达式:aN^2+bN+c
- 根据表达式获取复杂度原则舍弃bN+C忽略a得到的是N2得到O(N2) O在数学上是上限的含义
总结:评估性能的两个指标:时间复杂度,常数项执行时间
O最差,(-) 是平均,Ω是最优时间复杂度插入排序的最优是O(N)
1-2-1-3. 二分法O(logN)其实就是2为底
- 有序数组,某个数是否存在
- 在一个有序数组中,找>=某个数最左侧的位置
- 局部最小值问题
1-2-1-4. 对数器的概念和使用
- 有一个想要测的方法a
- 实现复杂度不好但是容易实现的方法b
- 实现一个随机样本产生器
- 把方法a和方法b跑相同的随机样本,看看结果是否一样
- 如果有一个随机样本使得对比结果不一致,打印样本进行人工干预,改对方法a或者方法b
- 当样本数量很多时对比测试一样正确,可以确认方法a已经正确。
1-2-1-5. 递归行为和递归行为时间复杂度的估算(P3认识O(NlogN)的排序)
用递归方法找一个数组中的最大值,系统上到底怎么做的?
master工时的使用
悬而未决的方法会压倒栈里去
1-2-1-5-1. master公式表达式说明
- T(N) 母问题的数据量是N:案例为arr的长度N
- N/b是子问题的规模都是N/b:案例是N/2;子问题数据是等量规模的。
- a是子问题被调用次数:案例是调用2次
- O(N^d)除去调用之外剩下的过程时间复杂度:案例是直接使用math函数返回是常数时间复杂度O(1);d=0
- public class Code08_GetMax {
- public static int getMax(int[] arr) {
- return process(arr,0,arr.length-1);
- }
- private static int process(int[] arr, int L, int R) { //process母问题规模N
- if(L==R){//arr[L...R]范围上只有一个数直接返回
- return arr[L];
- }
- /*
- * mid=(L+R)/2 中点一般这样计算,但是数组长度太大L+R可能溢出,mid可能算出负值下标
- * 可以写成mid=L+((R-L)/2) 除2可以换成位运算右移一位
- * */
- int mid=L+((R-L)>>1);//中点
- int leftMax=process(arr,L,mid);//子问题规模N/2
- int rightMzx=process(arr,mid+1,R);//子问题调用次数是2次
- return Math.max(leftMax,rightMzx);
- }
- }
如果logb^a<d 时间复杂度是O(N^d)如下图手写第一种就是上图master公式书写形式
1-2-1-5-2. 对数与指数
如果a^x =N(a>0,且a≠1),那么数x叫做以a为底N的对数,记作x=logaN,读作以a为底N的对数,其中a叫做对数的底数,N叫做真数。(上述master公式案例x=1所以常数时间复杂度是O(1))
1-2-1-5-3. MergeSort过程 归并排序时间复杂度O(NlogN)
- 分成左右两侧有序的子数组:2T(N/2)阶段
- 再顺序合并两个数据得到有序的数组:O(N)阶段
master
T(N)=2T(N/2)+O(N)
公式a=2,b=2,d=1符合loga^b=d时间复杂度是O(NlogN)
本地排局部数据落存有序分片其实就是有序的子数组,子数组后续合一进行归并排序合并不同机器数据。
让其整体有序的过程里用了外排序方法。额外空间复杂度O(N)
1-2-1-5-4. 小和问题(左侧比自己小的数)
最终1是小和数两次,2两次,3两次,4一次
最终和是1+1+3+2+2+3+4=16
左右数相等的时候,一定要先拷贝右组的数而且不产生小和。否则就不易知道有组有多少数比这个相等的数大。
逆序对(左边数比右边数大则两个数构成一个逆序对)
所有数据的处理都要考虑漏重乱序
1-2-2. 快速排序
最坏划分partition的值最偏O(N^2),如果划分值是中点就会O(NlogN)
1-2-2-1. 荷兰国旗问题
小于区域推着等于区域往右走撞上右侧区域往左走的位置:每次就会确认下被比较的数(默认从右边第一个数做第一个被比较的数叫base num case)的位置!!递归下去就会确认下所有被比较的数也就是所有的数的位置。
快排的空间复杂度o(logN) 最差是o(N)
1-3. 堆,桶排序及排序总结(P4)
1-3-1. 堆
- 堆结构就是用数组实现的完全二叉树结构
- 完全二叉树中如果每棵子树的最大值在顶部就是大根堆:有序
- 完全二叉树中如果每棵子树的最小值在顶部就是小根堆:有序
- 堆结构的heap Insert与heap ify操作
- 堆结构的增大和减少
- 优先级队列结构,就是堆结构
- //某个数出现在index上,往上继续移动
- 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;
- }
- }
- //某个数在index位置,能否往下移动
- /*
- *从任何位置都可以做heapify;heapSize限制数组大小,能确认左右孩子是否存在,越界不存在
- *1. 找数中的最大值(放到顶部成堆)
- * 2. 剩下的数据调整成大根堆
- * */
- private static void heapify(int[] arr, int index, int heapSize) {
- int left=index*2+1;//左孩子的下标
- while (left < heapSize) {//下方还有孩子的时候,left越界就没孩子,left小于右孩子下标
- //两个孩子中,谁值大,把下标给largest
- int largest=left+1<heapSize&&arr[left+1]>arr[left]?left+1:left;
- // 父和孩子之间,谁值大,把下标给largest
- largest=arr[largest]>arr[index]?largest:index;
- if(largest==index){
- break;
- }
- swap(arr,largest,index);
- index=largest;
- left=index*2+1;
- }
- }
左右孩子树最大值都比自己小或者自己没有左右孩子数就排序完毕成大根堆。
heapify往下调整,heapInsert往上调整数
为什么时间复杂度都是logN因为无论是插入还是移除后调整成大根堆都是调整树的高度,高度和数的数量N的关系就是logN函数
堆的调整都是高度的调整所以是logN
满二叉树:最底层节点=N/2,每个节点heapify往下移动0次,但是循环了一次进行了1次操作代价是1
倒数第二层:N/4,代价是2;如下图
1-3-1-1. 几乎有序数组排序:准备k的小根堆时间复杂度o(N*logK)
1-3-1-1-1. 以10为底
- 当0<n<10 的时候 nlogn<n
- 当n=10 的时候 nlogn=n
- 当n>10 的时候 nlogn>n
也就是说log函数的幂>底数时N*logN会>N;时间复杂度偏高。当幂和底数(底数不能是0和1)相同log函数值为1得O(1);
1-3-1-1-2. 手写堆的需求场景
- 手写堆的场景需求:形成堆的数据需要指定位置的数据进行修改,手写可以以最少代价减少heapify,heapinsert次数;
- 但是系统的优先级队列不支持,这种调整数据的需求,因为它的代价太高要重新所有数据进行多次的heapify
1-3-2. 比较器(第三天内容)
1-3-2-1. 比较器的使用
- 比较器(JAVA)的实质就是重载比较运算符(C++)
- 比较器可以很好的应用在特殊标准的排序上
- 比较器可以很好的应用在根据特殊标准排序的结构上
1-3-2-2. 排序总结
- 基于比较排序
- 不基于比较排序与数据状况有关
1-3-2-2-1. 计数排序
1-3-2-3. 基数排序
- 比如按照进制数据结构状况的数进行排序17变成017,100等进行排序保证数据位数同等级。
桶的排序需要基于样本数据满足桶的划分才能用桶排序。
- package leecode.zuochengyun.dayof7;
- /**
- * create-date:2022/5/27
- * author:guojia.ma
- * 不通过比较的排序1。计数排序2.基数排序:此处案例桶排序
- */
- public class Code02_RadixSort {
- // only for no-neagtive value
- public static void radixSort(int[] arr) {
- if(arr==null||arr.length<2){
- return;
- }
- radixSort(arr,0,arr.length-1,maxbits(arr));
- }
- //arr[begin..end]排序
- private 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 = 0; d <=digit; d++) {//有多少位就每个数字进出桶多少次
- // 10个空间
- // count[0] 当前位(d位)是0的数字有多少个
- // count[1] 当前位(d位)是(0和1)的数字有多少个
- // count[i] 当前位(d位)是(0~i)的数字有多少个
- int[] count=new int[radix];//count[0..9]
- for ( i = 0; i < radix; i++) {
- j=getDigit(arr[i],d);
- count[j]++;
- }
- for ( i = 0; 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]=arr[i];//辅助数组
- count[j]--;
- }
- for (i = L,j =0; i<=R;i++,j++) {
- arr[i]=bucket[j];//把bucket出桶的结果数怼回到原数组,
- }
- }//循环其他位进行入桶出桶怼回原数组
- }
- private static int getDigit(int x, int d) {
- return ((x/((int)Math.pow(10,d-1)))%10);
- }
- private 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;
- }
- }
1-4. 链表(P5)
1-4-1. 排序算法的稳定性(5-5相等不发生位置相对次序变更)及其汇总
- 同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定性的;否则就没有。
- 不具备稳定性的排序:选择排序,快速排序,堆排序
- 具备稳定性的排序:冒泡排序,插入排序,归并排序,一切桶排序思想下的排序
目前:基于比较的排序没有找到时间复杂度O(N*logN);额外空间复杂度O(1)和稳定性的排序二选一的排序。
1-4-1-1. 常见坑
- 归并排序的额外空间度为O(1),有兴趣可以搜“归并排序”内部缓存法=》将导致稳定性消失,不如直接使用堆排序!!
- "原地归并排序“的帖子都是垃圾,会让归并排序时间复杂度变成o(N^2),不如直接使用插入排序
- 快速排序可以zuo8dao稳定性问题,但是非常难,可以搜“01 stable sort”=》空间复杂度将上升到O(N)不如直接使用归并排序
- 有一道题目,是奇数放在数组左边,偶数放在数组右边,要求原始的相对次序不变,碰到这个问题,可以怼面试官。
1-4-1-2. 工程上排序改进
- 充分利用O(N*logN)和O(N^2)排序各自的优势
- 稳定性的考虑
一般基础类型不考虑稳定性用快排,复杂类型可能要求稳定性用归并排序.
1-4-2. hash表
- 哈希表在使用层面上可以理解为一种集合结构
- 如果只有key,没有伴随数据value,可以使用hashSet结构(C++中叫UnOrderedSet)
- 如果既有key,又有伴随value,可以使用HashMap结构(C++中叫UnOrderedMap)
- 有无伴随数据是map和set的唯一区别,底层的实际结构是一样的。
- 使用hash表 增删改查都是时间复杂度都是O(1)但是常数时间级别比较大。
- 放入hash表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
- 放入hash表的东西,如果不是基础类型,内部按照引用传递,内存占用是这个东西内存地址的大小。
有关hash表原理将在提升班hansh函数有关的数据结构一章讲解。
1-4-2-1. 有序表介绍
- 有序表在使用层面上可以理解为一种集合结构
- 如果只有key,没有伴随value,可以使用TreeSet结构(C++叫OrderedSet)
- 如果既有key,又有value,可以使用TreeMap(C++叫OrderedMap)结构
- 有无伴随数据,是TreeSet和TreeMap的唯一区别,底层实际是一回事。
- 红黑树,AVL数,size-balance-tree(傻逼树?)和跳表等都属于有序表结构,只是底层具体实现不同。
- 放入hash表,是基础类型值传递,内存是这个东西的大小(值大小)
- 放入hash表,不是基础类型,必须提供比较器,引用传递,内存是地址的大小
- 不管什么底层实现,只要是有序表,都有以下固定的基本公共能力和固定的时间复杂度。
1-4-2-1-1. 有序表的固定操作
- void put(key,value):讲一个(k,v)记录加入到表中,获奖k的记录更新成v.
- v get(k): 根据给定的k,查询v返回
- void remove(k):移出k的记录
- boolean containsKey(k):询问是否有关于k的记录
- K firstKye():返回所有键值的排序结果中,最左(小)的那个
- K lastKey():返回所有键值的排序结果中,最右(大)的那个
- K floorKey(k):如果表中存在key返回k,否则返回所有键值的排序结果中,key的前一个。
- K ceilingKey(k):如果表中存在key返回k,否则返回所有键值的排序结果中,key的后一个。
以上所有操作时间复杂度都是O(logN),N为有序表含有的记录数,有关有序表原理,将在有序表详解一章讲述。涉及原理的题一般都是难题;
1-4-3. 链表
1-4-4. 反转
- 题目:分别实现翻转单向链表和反转双向链表的函数
要求:如果链表长度为N,时间复杂度要求O(N),额外空间复杂度O(1)
如果链表需要换头就要方法带返回值,否则直接void即可。head=f(head).
1-4-4-1. 打印有序链表公共部分
- 题目:给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
要求:如果两个链表的长度之和为N,时间复杂度要求O(N),额外空间复杂度O(1)
1-4-4-2. 链表解体方法论
- 对于笔试,不用太在乎空间复杂度,一切为了时间复杂度
- 对于面试,时间复杂度放在第一位,但是一定要找到空间最省的方法
1-4-4-2-1. 额外技巧:
- 额外数据机构记录(笔试快速写)(hash表等)
- 快慢指针(一定要自己练coding:跨的步子不一样比如快指针走2步慢走1步,快走完了慢走到中间位置(要根据长度是奇数还是偶数定制))
1-4-4-3. 回文结构(笔试:栈结构)
题目:给定一个单链表的头结点head,请判断该链表是否为回文结构。
例子:1-》2-》1,返回true,1->2->2-1返回true,15->6->15 返回true,1-》2->3返回false.
面试要求:如果链表长度为N,时间复杂度达到O(N),额外空间复杂度为O(1):用链表,用几个变量的额外空间(主要考察coding能力)
1-4-4-4. 讲单链表按某值分成左边小,中间等,右边大的形式
题目:给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数privot.实现一个调整链表的函数,讲链表调整为左部分都是值小于privot的节点,中间部分都是值等于privot的节点,有部分都是值大于privot的节点。
进阶:在实现原问题功能基础上增加如下要求
要求1:调整后所有小于privot的节点之间的相对顺序和调整前一样
要求2:调整后所有等于privot的节点之间的相对顺序和调整前一样
要求3:调整后所有大于privot的节点之间的相对顺序和调整前一样
要求4:时间复杂度达到0(N),额外空间复杂度请达到0(1)
- public static Node listPartition2(Node head,int privot){
- Node sH=null;//small head
- Node sT=null;//small tail
- Node eH=null;//equal head
- Node eT=null;//equal tail
- Node mH=null;//big head
- Node mT=null;//big tail
- Node next=null;//save next node
- //every node distributed to three lists
- while(head!=null){
- next=head.next;
- head.next=null;
- if(head.value<privot){
- if(sH==null){
- sH=head;
- sT=head;
- }else{
- sT.next=head;
- sT=head;
- }
- }else if(head.value==privot){
- if(eH==null){
- eH=head;
- eT=head;
- }else{
- eT.next=head;
- eT=head;
- }
- }else{
- if(mH==null){
- mH=head;
- mT=head;
- }else {
- mT.next=head;
- mT=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=mH;
- }
- return sH!=null?sH:(eH!=null?eH:mH);
- }
1-4-4-5. 复制含有随机指针节点的链表
- public static class Node{
- public int value;
- public Node next;
- public Node rand;
- public Node(int data){
- this.value=data;
- }
- }
- public static Node copyListWithRand1(Node head){
- HashMap<Node, Node> map = new HashMap<>();
- Node cur=head;
- while (cur!=null){
- map.put(cur,new Node(cur.value));
- cur=cur.next;
- }
- cur=head;
- while (cur!=null){
- //cur 老
- //map.get(cur)//新
- map.get(cur).next=map.get(cur.next);
- map.get(cur).rand=map.get(cur.rand);
- cur=cur.next;
- }
- return map.get(head);
- }
1-4-4-6. 单链表相交的一系列问题(单链表最难)
题目:给定两个可能有环也可能无环的单链表,头结点head1和head2.请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null
要求:如果两个链表长度和未N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)
- /**
- * create-date:2022/6/15
- * author:guojia.ma
- * 单链相交节点
- */
- public class Code07_FindFirstIntersectNode {
- public static class Node {
- public int value;
- public Node next;
- public Node(int data){
- this.value=data;
- }
- }
- 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){//两个链表都有环:环可以独立,可以同一个环:同一个环也可以是不同入环节点loopN
- return bothLoop(head1,loop1,head2,loop2);
- }
- return null;
- }
- // 两个有环链表,返回第一个相交节点(相对任意一个链表的第一个相交),如果不想交返回null
- private static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
- Node cur1=null;
- Node cur2=null;
- if(loop1==loop2){
- cur1=head1;
- cur2=head2;
- int n=0;
- while (cur1!=loop1){
- n++;
- cur1=cur1.next;
- }
- while(cur2!=loop2){
- n--;
- cur2=cur2.next;
- }
- cur1=n>0?head1:head2;
- cur2=cur1==head1?head2:head1;
- n=Math.abs(n);
- while(n!=0){
- n--;
- cur1=cur1.next;
- }
- while(cur1!=cur2){
- cur1=cur1.next;
- cur2=cur2.next;
- }
- return cur1;
- }else{
- cur1=loop1.next;
- while(cur1!=loop1){
- if(cur1==loop2){//cur1遇到loop2即第二个链的相交节点
- return loop1;
- }
- cur1=cur1.next;
- }
- return null;
- }
- }
- // 如果两个链表都无环,返回第一个相交节点如果不相交,返回null
- private 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.next!=null){
- n++;
- cur1 = cur1.next;
- }
- while(cur2.next!=null){
- n--;
- cur2=cur2.next;
- }
- if(cur1!=cur2){//如果两个链表末尾节点不同就不想交
- return null;
- }
- cur1=n>0?head1:head2;//谁长,谁的头变成cur1
- cur2=cur1==head1?head2:head1;//谁短,谁的头变成cur2
- n=Math.abs(n);//两个链表的长度差值,让长的链表先走n;两个再2一期走1步步走,最终在相交点相碰
- while (n!=0){
- n--;
- cur1=cur1.next;
- }
- while(cur1!=cur2){
- cur1=cur1.next;
- cur2=cur2.next;
- }
- return cur1;
- }
- // 找到链表第一个入环节点,如果无环,返回null
- private static Node getLoopNode(Node head) {
- if(head==null||head.next==null||head.next.next==null){
- return null;
- }
- Node n1 = head.next;//n1->slow 指针
- Node n2 = head.next.next;//n2->fast 快指针
- while(n1!=n2){//如果存在环,快慢指针相遇就在环的第一个节点
- if(n2.next==null||n2.next.next==null){
- return null;//如果快指针提前走到尾2,就不存在环2
- }
- n2=n2.next.next;
- n1=n1.next;
- }
- n2=head;//n2->walk again from head
- while (n1!=n2){
- n1=n1.next;
- n2=n2.next;
- }
- return n1;
- }
- }
1-5. 二叉树(P6)
class Node<v>{
V value;
Node left;
Node right;
}
- 用递归和非递归两种方式实现二叉树的先序,中序,后序2遍历
- 如何只管的打印一颗二叉树
- 如何完成二叉树的宽度优先遍历(常见题目:求一颗二叉树的宽度)
1-5-1. 递归序
基于递归序可以加工出先中后三种遍历顺序。
1-5-1-1. 递归的先中后遍历
- package leecode.zuochengyun.dayof7;
- import java.util.Stack;
- /**
- * create-date:2022/6/15
- * author:guojia.ma
- * 二叉树的基于递归的先中后遍历
- */
- public class Code01_PreInPosTraversal {
- public static class Node{
- public int value;
- public Node left;
- public Node right;
- public Node(int data){
- this.value=data;
- }
- }
- //先序遍历
- public static void preOrderRecur(Node head){
- if(head==null){
- return;
- }
- System.out.print(head.value+" ");
- preOrderRecur(head.left);
- preOrderRecur(head.right);
- }
- //中序遍历
- public static void inOrderRecur(Node head){
- if(head==null){
- return;
- }
- inOrderRecur(head.left);
- System.out.print(head.value+" ");
- inOrderRecur(head.right);
- }
- //后序遍历
- public static void posOrderRecur(Node head){
- if(head==null){
- return;
- }
- posOrderRecur(head.left);
- posOrderRecur(head.right);
- System.out.print(head.value+" ");
- }
- // 非递归先序遍历
- public static void preOrderUnRecur(Node head){
- System.out.print("pre-order: ");
- if(head!=null){
- Stack<Node> stack = new Stack<Node>();
- stack.add(head);
- while(!stack.isEmpty()){
- head = stack.pop();
- System.out.print(head.value+" ");
- if(head.right!=null){
- stack.push(head.right);
- }
- if(head.left!=null){
- stack.push(head.left);
- }
- }
- }
- System.out.println();
- }
- }
二叉树的先序遍历就是深度遍历
宽度遍历用队列;从头进,先放左再放右;然后从尾开始依次弹出。
1-5-1-2. 二叉树的相关概念及实现判断
- 如何判断一颗二叉树是否是搜索二叉树(BST ):左比父小,右比父大,中序遍历升序?
- // 判断是否是搜索二叉树BST
- public static int preValue=Integer.MIN_VALUE;
- public static boolean checkBST(Node head){
- if(head==null){
- return true;
- }
- boolean isLeftBst=checkBST(head.left);
- if(!isLeftBst){
- return false;
- }
- if(head.value<=preValue){
- return false;
- }else {
- preValue=head.value;
- }
- return checkBST(head.right);
- }
- 如何判断一颗二叉树是完全二叉树:从左到右依次变满?
- // 判断是否是完全二叉树CBT:宽度有限遍历
- public static boolean isCBT(Node head){
- if(head==null){
- return true;
- }
- LinkedList<Node> queue = new LinkedList<>();
- // 是否遇到过左右两个孩子不双全的节点
- boolean leaf=false;
- Node l=null;
- Node 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)){//有右无左
- return false;
- }
- if(l!=null){
- queue.add(l);
- }
- if(r!=null){
- queue.add(r);
- }
- if(l==null||r==null){
- leaf=true;
- }
- }
- return true;
- }
-
如何判断一颗二叉树是否是满二叉树?最大深度L,节点数N满足:N=2^L-1
-
如何判断一颗二叉树是否是平衡二叉树?(二叉树题目套路):任何一个子树左树和右树的高度差不超过1
- package leecode.zuochengyun.dayof7;
- /**
- * create-date:2022/6/16
- * author:guojia.ma
- * 平衡二叉树判断
- */
- public class Code06_IsBalancedTree {
- public static class Node{
- public int value;
- public Node left;
- public Node right;
- public Node(int data){
- this.value=data;
- }
- }
- public static boolean isFull(Node head){
- ReturnData allInfo=p(head);
- return (1<<allInfo.height-1)==allInfo.nums;
- }
- public static class ReturnData{
- public int height;
- public int nums;
- public ReturnData(int h,int n){
- height=h;
- nums=n;
- }
- }
- public static ReturnData p(Node x){
- if(x==null){
- return new ReturnData(0,0);
- }
- ReturnData leftData=p(x.left);
- ReturnData rightData=p(x.right);
- int height=Math.max(leftData.height,rightData.height)+1;
- int nums=leftData.nums+rightData.nums+1;
- return new ReturnData(height,nums);
- }
- public static boolean isBalanced(Node head){
- return process(head).isBalanced;
- }
- public static class ReturnType{
- public boolean isBalanced;
- public int height;
- public ReturnType(boolean isB,int hei){
- isBalanced=isB;
- height=hei;
- }
- }
- public static ReturnType process(Node x){
- if(x==null){//base case 空树
- return new ReturnType(true,0);
- }
- ReturnType leftData=process(x.left);
- ReturnType 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 ReturnType(isBalanced,height);
- }
- /* public static class Info{
- public boolean isBST;
- }*/
- }
1-5-1-2-1. 二叉树递归套路
- 列可能性,左树需要获取多少信息,右树性需要什么信息:比如平衡二叉树就要获取左右两个树是否平?高度多少?递归返回值就是这个信息数据class
所有的树型的DP(动态规划)都是按照这个套路进行coding解决。
1-5-2. 给定两个二叉树的节点node1和node2,找到他们的最低公共祖先节点(网上最早汇聚的点 )
- public static Node lowestAncestor(Node head,Node o1,Node o2){
- if(head==null ||head==o1||head==o2){//base case
- return head;
- }
- Node left = lowestAncestor(head.left, o1, o2);
- Node right = lowestAncestor(head.right, o1, o2);
- // 对于情况2
- if(left!=null&&right!=null){
- return head;
- }
- //对于情况1 两个树,并不都有返回值;一个树既没有o1,o2一定返回空
- return left!=null?left:right;
- }
1-5-2-1. 后继节点(中序遍历中下一个节点)
在二叉树中找到一个几点的后继几点
题目:现在有一种新的二叉树节点类型如下:
pubic class Node{
public int value;
public Node left;
public Node right;
public Node parent;
public Node(int val){
value=val;
}
}
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假设有一颗Node类型的节点组成的二叉树,树2中每个节点的parent指针都正确地指向自己的父节点,头结点的parent指向null.
只给一个在二叉树中某个节点node,请实现返回node的后继节点的函数。
在二叉树的中序遍历序列中,node的下一个节点叫做node的后继节点。
- public static Node getSuccessorNode(Node node){
- if(node==null){
- return node;
- }
- if(node.right!=null){//情况1
- return getLeftMost(node.right);
- }else{//情况2 无右子树
- Node parent = node.parent;
- while (parent!=null&&parent.left!=node){//情况3 当前节点是其父节点右孩子
- node=parent;
- parent=node.parent;
- }
- return parent;
- }
- }
- private static Node getLeftMost(Node node) {
- if(node==null){
- return node;
- }
- while (node.left!=null){
- node=node.left;
- }
- return node;
- }
1-5-2-2. 二叉树的序列化和反序列化
就是内存里的一颗树如何变成字符串形式,又如何从字符串变成内存里的树?
如何判断一颗二叉树是不是另一颗二叉树的子树?
- // 以head为头的树,请序列化成字符串返回//以下递归顺序是先序遍历及序列化
- public static String serialByPre(Node head){
- if(head==null){
- return "#_";
- }
- String res=head.value+"_";
- res+=serialByPre(head.left);
- res+=serialByPre(head.right);
- return res;
- }
- // 反序列化
- public static Node reconByPreString(String preStr){
- String[] values = preStr.split("_");
- Queue<String> queue = new LinkedList<>();
- for (int i = 0; i !=values.length; i++) {
- queue.add(values[i]);
- }
- return reconPreOrder(queue);
- }
- public static Node reconPreOrder(Queue<String> queue) {
- String value=queue.poll();
- if(value.equals("#")){
- return null;
- }
- Node head = new Node(Integer.valueOf(value));
- head.left = reconPreOrder(queue);
- head.right = reconPreOrder(queue);
- return head;
- }
1-5-2-3. 微软折纸题
- public static void printAllFolds(int N) {
- printProcess(1,N,true);
- }
- /*
- * 递归过程,来到了某一个节点
- * i是节点的层数,N是一共的层数,down==true 凹,down==false 凸
- * */
- public static void printProcess(int i, int N, boolean down) {
- if(i>N){
- return;
- }
- printProcess(i+1,N,true);
- System.out.println(down?"凹":"凸");
- printProcess(i+1,N,false);
- }
- public static void main(String[] args) {
- int N=3;
- printAllFolds(3);
- }
1-6. 图(P8)
1-6-1. 图的存储方式
- 邻接表
- 邻接矩阵
如何表达图?生成图?
表达图的结构比较多,算法coding就要变,但是算法还是原来的算法。
- package leecode.zuochengyun.dayof7;
- import java.util.HashMap;
- import java.util.HashSet;
- /**
- * create-date:2022/6/17
- * author:guojia.ma
- * 基础班:图
- */
- public class Graph {
- public HashMap<Integer,Node> nodes;
- public HashSet<Edge> edges;
- public Graph(){
- nodes=new HashMap<>();
- edges=new HashSet<>();
- }
- }
- package leecode.zuochengyun.dayof7;
- import java.util.ArrayList;
- /**
- * create-date:2022/6/17
- * author:guojia.ma
- * 图的点
- */
- public class Node {
- public int value;
- 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<>();
- }
- }
- package leecode.zuochengyun.dayof7;
- /**
- * create-date:2022/6/17
- * author:guojia.ma
- * 图的边
- */
- 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;
- }
- }
- package leecode.zuochengyun.dayof7;
- /**
- * create-date:2022/6/17
- * author:guojia.ma
- * 把数据转化成图结构
- */
- public class GraphGenerator {
- /*
- * matrix所有的边
- * N*3的矩阵
- * 【weigbht,from节点上面的值,to节点上面的值】
- * */
- public static 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);
- fromNode.out++;
- toNode.in++;
- fromNode.edges.add(newEdge);
- graph.edges.add(newEdge);
- }
- return graph;
- }
- }
1-6-2. 图的宽度和广度优先遍历
1-6-2-1. 宽度优先
- 利用队列实现
- 从源节点开始依次按照宽度进队列,然后弹出
- 每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
- 直到队列变空
- // 从node出发,进行宽度优先遍历
- public static void bfs(Node node){
- if(node==null){
- return;
- }
- Queue<Node> queue = new LinkedList<>();
- HashSet<Node> set=new HashSet<>();
- queue.add(node);
- set.add(node);
- while (!queue.isEmpty()){
- Node cur = queue.peek();
- System.out.println(cur.value);
- for (Node next:cur.nexts
- ) {
- if(!set.contains(next)){
- set.add(next);
- queue.add(next);
- }
- }
- }
- }
1-6-2-2. 广度(深度)优先
- 利用栈实现
- 从源节点开始把节点按照深度放入栈,然后弹出
- 每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
- 直到栈变空。
- public static void dfs(Node node){
- if(node==null){
- return;
- }
- Stack<Node> stack = new Stack<>();
- HashSet<Node> set = new HashSet<>();
- stack.add(node);//当前节点入栈,入set
- 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);//注册邻居节点(没有遍历过的遍历并注册,已经遍历过的就注册过,只不过从新放入栈中,后面直接while弹出来即可。)
- System.out.println(next.value);
- break;
- }
- }
- }
- }
1-6-3. 拓扑排序算法
使用范围:要求有向图,且入度为0的节点,且没有环。
- // directed grouph and no loop
- public static 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);
- }
- }
- // 拓扑排序的结果,一次加入result
- List<Node> result=new ArrayList<>();
- while(!zeroInQueue.isEmpty()){
- Node cur = zeroInQueue.poll();
- result.add(cur);
- for (Node next:cur.nexts
- ) {
- inMap.put(next,inMap.get(next)-1);//擦除上个入度为0的后续节点的影响
- if(inMap.get(next)==0){
- zeroInQueue.add(next);//把入度为0的放到result里
- }
- }
- }
- return result;
- }
1-6-4. 最小生成树:权值和最小的边保留
- // Union-Find Set
- public static class UnionFind {
- // key 某一个节点,value key节点往上的节点
- private HashMap<Node,Node> fatherMap;
- // key 某一个集合的代表节点,value key所在集合的节点个数
- private HashMap<Node,Integer> sizeMap;
- public UnionFind(){
- fatherMap=new HashMap<Node,Node>();
- sizeMap=new HashMap<Node,Integer>();
- }
- public void makeSets(Collection<Node> nodes){
- fatherMap.clear();
- sizeMap.clear();
- for (Node node:nodes
- ) {
- fatherMap.put(node,node);
- sizeMap.put(node,1);
- }
- }
- private Node findFather(Node n){
- Stack<Node> path=new Stack<>();
- while(n!=fatherMap.get(n)){
- path.add(n);
- n=fatherMap.get(n);
- }
- while (!path.isEmpty()){
- fatherMap.put(path.pop(),n);
- }
- return n;
- }
- public boolean isSameSet(Node a,Node b){
- return findFather(a)==findFather(b);
- }
- public void union(Node a,Node b) {
- if (a == null || b == null) {
- return;
- }
- Node aDai = findFather(a);
- Node bDai = findFather(b);
- if (aDai != bDai) {
- int aSetSize = sizeMap.get(aDai);
- int bSetSize = sizeMap.get(bDai);
- if (aSetSize <= bSetSize) {
- fatherMap.put(aDai, bDai);
- sizeMap.put(bDai, aSetSize + bSetSize);
- sizeMap.remove(aDai);
- } else {
- fatherMap.put(bDai, aDai);
- sizeMap.put(aDai, aSetSize + bSetSize);
- sizeMap.remove(bDai);
- }
- }
- }
- }
- public static class EdgeComparator implements Comparator<Edge> {
-
- public int compare(Edge o1, Edge o2) {
- return o1.weight-o2.weight;
- }
- }
1-6-4-1. kruskal算法(K算法) :并集查
以边的角度出发,边排序,一个个加并判断是否成环,成环就不加;判断是否成环就要靠一种数据结构
使用范围:要求无向图;
- public static Set<Edge> kruskalMST(Graph graph){
- UnionFind unionFind = new UnionFind();
- unionFind.makeSets(graph.nodes.values());
- PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
- for (Edge edge:graph.edges
- ) {//M条边
- priorityQueue.add(edge);//O(logM)
- }
- Set<Edge> result=new HashSet<>();
- while(!priorityQueue.isEmpty()){
- Edge edge = priorityQueue.poll();//M条边
- if(!unionFind.isSameSet(edge.from,edge.to)){//o(1)
- result.add(edge);
- unionFind.union(edge.from,edge.to);
- }
- }
- return result;
- }
1-6-4-2. prim算法(P算法):从点角度考虑
- public static class EdgeComaprator implements Comparator<Edge>{
-
- public int compare(Edge o1, Edge o2) {
- return o1.weight-o2.weight;
- }
- }
- public static Set<Edge> primMST(Graph graph){
- // 解锁的边2进入小根堆:按边大小的顺序放边=》放所有解锁的边。
- PriorityQueue<Edge> priorityQueue=new PriorityQueue<>(new EdgeComaprator());
- HashSet<Node> set=new HashSet<>();
- Set<Edge> resul=new HashSet<>();//依次挑选的最小边在result里:不在set的点就是新的点
- for (Node node:graph.nodes.values()
- ) {//随便挑一个点
- // node是开始点
- if(!set.contains(node)){
- set.add(node);
- for (Edge edge: node.edges
- ) {//由一个点解锁所有相连的边
- priorityQueue.add(edge);
- }
- while(!priorityQueue.isEmpty()){
- Edge edge = priorityQueue.poll();//弹出解锁的边中,最小的边
- Node toNode = edge.to;//可能的一个新的点
- if(!set.contains(toNode)){//不含有的时候,就是新的点
- set.add(toNode);
- resul.add(edge);
- for (Edge nextEdge: toNode.edges
- ) {
- priorityQueue.add(nextEdge);//可能一条边会被重复方队列,不影响最后结论
- }
- }
- }
- }
- }
- return resul;
- }
1-6-5. Di jkstra算法(单元最短路径算法)
- 堆的改写特征,给堆一个,堆吐出一个;不需要再改堆的值;系统能够实现改写,但是是全表扫描代价比较高,最好手动改写,通过向上向下手动调整,可以减少调整的代价。
适用范围:没有权值为负数的环。
- public static HashMap<Node,Integer> dijkstral(Node head){
- // 从head触发到所有点的最小距离
- // key: 从head出发到达key
- // value: 从head出发到达可以的最小距离
- // 如果在表中,没有T的记录,含义是从head出发到T这个点的距离为正无穷
- HashMap<Node,Integer> distanceMap=new HashMap<>();
- distanceMap.put(head,0);
- // 已经求过距离的点,存在selectedNodes中,以后再也不碰,锁死
- HashSet<Node> selectedNodes=new HashSet<>();
- Node minNode= getMinDistanceAndUnselectedNode(distanceMap,selectedNodes);
- while (minNode!=null){
- int distance=distanceMap.get(minNode);
- for (Edge edge:minNode.edges
- ) {
- Node toNode=edge.to;
- if(!distanceMap.containsKey(toNode)){
- distanceMap.put(toNode,distance+edge.weight);
- }
- distanceMap.put(edge.to,Math.min(distanceMap.get(toNode),distance+edge.weight));//到达key最小的距离记录
- }
- selectedNodes.add(minNode);//选过的点
- minNode=getMinDistanceAndUnselectedNode(distanceMap,selectedNodes);
- }
- return distanceMap;
- }
- //找出map中最小距离的kv,但是key不能是已经选过的
- public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> touchedNodes) {
- Node minNode=null;
- int minDistance=Integer.MAX_VALUE;
- for (Map.Entry<Node,Integer> entry:distanceMap.entrySet()
- ) {
- Node node=entry.getKey();
- int distance=entry.getValue();
- if(!touchedNodes.contains(node)&&distance<minDistance){
- minNode=node;
- minDistance=distance;
- }
- }
- return minNode;
- }
1-7. 前缀树(标题P9第8章暴力递归):实际是第7章:前缀树和贪心算法;内容顺序有一章节延后
1-7-1. 前缀树
何为前缀树?如何生成前缀树?
例子:
一个字符串类型的数组arr1,另一个字符串类型的数字arr2.arr2中有哪些字符,是arr1中出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。请打印arr2中出现次数最大的前缀。
- public static class TrieNode{
- public int pass;
- public int end;
- public TrieNode[] nexts;//HashMap<Char,Node> nexts;
- public TrieNode(){
- pass=0;
- end=0;
- // nexts[0]==null 没有走向‘a’的路
- // nexts[0]!=null 有走向‘a’的路
- //...
- //nexts[25]!=null 有走向‘z’的路
- nexts=new TrieNode[26];
- }
- }
- public static class Trie{
- private TrieNode root;
- public Trie(){
- root=new TrieNode();
- }
- public void insert(String word){
- if(word==null){
- return;
- }
- char[] chs=word.toCharArray();
- TrieNode node=root;
- node.pass++;
- int index=0;
- for (int i = 0; i < chs.length; i++) {//从左往右遍历字符
- index=chs[i]-'a';//由字符,对应成走向哪条路
- if(node.nexts[index]==null){
- node.nexts[index]=new TrieNode();
- }
- node=node.nexts[index];
- node.pass++;
- }
- node.end++;
- }
- public void delete(String word){
- if(search(word)!=0){//确定树中确实加入过word,才删除
- char[] chs=word.toCharArray();
- TrieNode node=root;
- node.pass--;
- int index=0;
- for (int i = 0; i < chs.length; i++) {
- index=chs[i]-'a';
- if(--node.nexts[index].pass==0){
- // JAVA C++要遍历到底去析构
- node.nexts[index]=null;//JAVA=null就是jvm垃圾回收析构了,C++需要一个个遍历处理
- // ...
- return;
- }
- node=node.nexts[index];
- }
- node.end--;
- }
- }
- // 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.nexts[index]==null){
- return 0;
- }
- node=node.nexts[index];
- }
- return node.end;
- }
- // 所有加入的字符串中,有几个是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.nexts[index]==null){
- return 0;
- }
- node=node.nexts[index];
- }
- return node.pass;
- }
- }
1-7-2. 贪心算法
在某一个标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫做贪心算法。
也就是说,不从整体最优上加以考虑,所作出的是在某种意义上的局部最优解。
局部最优-?->整体最优。没到题局部最优到整体最优的证明都不同。
1-7-2-1. 题目:会议问题
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲场次最多。返回这个最多的宣讲场次。
解体思路:哪个会议结束时间早,最先安排,不能安排的会议删掉。
- public static class Program{
- public int start;
- public int end;
- public Program(int start,int end){
- this.start=start;
- this.end=end;
- }
- }
- public static class ProgramComparator implements Comparator<Program> {
-
- public int compare(Program o1, Program o2) {
- return o1.end-o2.end;
- }
- }
- 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;
- }
1-7-2-1-1. 贪心算法在笔试时的解题思路(对数器证明贪心算法局部最优-》整体最优的证明)
- 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
- 脑补出贪心策略A,贪心策略B,贪心策略C。。。
- 用解法X和对数期,取验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
- 不要取纠结贪心策略的证明(可以举反例证明某一种贪心错误)
1-7-2-2. 题目:字典顺序最小
- public static class MyComparator implements Comparator<String>{
-
- public int compare(String a, String b) {//比较策略(有传递性)就是贪心策略,需证明这个策略有效
- return (a+b).compareTo(b+a);//前一个字典数更低返回负数,否则返回正数
- }
- }
- /*
- *传递性的数据比较策略就是正确的贪心策略的证明就是以下满足:=》前后结果传递性满足
- * a.b<=b.a
- * b.c<=c.b
- * =>a.c<=c.a
- * 最终得到数学函数公式:
- */
- public static String lowestString(String[] strs){
- if(strs==null||strs.length==0){
- return "";
- }
- Arrays.sort(strs,new MyComparator());
- String res="";
- for (int i = 0; i < strs.length; i++) {
- res+=strs[i];
- }
- return res;
- }
传递性的数据比较策略就是正确的贪心策略的证明就是以下满足:=》前后结果传递性满足
证明比较策略的传递性图解:
贪心策略在实现时,经常用到的技巧:
- 根据某标准建立一个比较器来排序
- 根据某标准建立一个比较器来组成堆
1-7-2-3. 题目六:切金条(哈夫曼编码树)
一块金条长度切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的jintiao9,不管启程长度多大的两半,都要花费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个铜板。
输入一个数组,返回分割的最小代价。
- public static int lessMoney(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;
- }
- public static class MinheapComparator implements Comparator<Integer>{
-
- public int compare(Integer o1, Integer o2) {
- return o1-o2;//<0 o1<o2负数
- }
- }
贪心策略:笔试概率大,考coding,一般5道题的最后一个;但是面试少,因为没有办法考coding,证明起来又比较难。
1-7-2-4. 题目八
- 贪心策略没有区分度(策略选对就对了,没有所谓的coding能力),如果是为了淘汰率可以出。
输入:
正数数组costs
正数数组profits
正数k,m
含义:
costs[i]标识i号项目的花费,profits[i] 标识i号项目在扣除花费之后还能挣到的钱(利润)
k标识你只能串行的最多做k个项目,m标识你初始的资金。
说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。
输出:你最后获得的最大钱数。
- public static class Node{
- public int p;
- public int c;
- public Node(int p,int c){
- this.p=p;
- this.c=c;
- }
- }
- public static class MinCostComparator implements Comparator<Node>{
-
- public int compare(Node o1, Node o2) {
- return o1.c-o2.c;
- }
- }
- public static class MaxProfitComparator implements Comparator<Node>{
-
- public int compare(Node o1, Node o2) {
- return o1.p-o2.p;
- }
- }
- public static int findMaximizedCaptial(int k,int W,int[] Profits,int[] Captial){
- 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],Captial[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;
- }
1-8. 堆问题 题目:一个数据流中,随时取得中位数
1-9. 暴力递归
1-9-1. 题目 N皇后问题
N皇后问题是指在N*N的期盼上要摆N个皇后,要求任何两个皇后不同行,不同列,也不再同一条斜线上。
给定一个整数n,返回n皇后的摆法有多少种。
n=1,返回1
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
n=8,返回92.
- /*优化常数项:2进制的树只使用位信息不使用树的值*/
- // 请不要超过32皇后问题,位运算最好不要超过32位所以限制32,否则类型换成long
- public static int num2(int n){
- if(n<1||n>32){
- return 0;
- }
- int limit=n==32?-1:(1<<n)-1;
- return process2(limit,0,0,0);
- }
- /*
- * colLim 列的限制,1的位置不能放皇后,0的位置可以
- * leftDiaLim 左斜线的限制,1的位置不能方皇后,0的位置可以:通过左位移一位
- * rightDiaLim 右斜线的限制,1的位置不能方皇后,0的位置可以:通过右位移一位
- * */
- public static int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim) {
- if(colLim==limit){//base case n个皇后而且都合法
- return 1;
- }
- int pos=0;
- int mostRightOne=0;
- pos=limit&(~(colLim|leftDiaLim|rightDiaLim));//三个或后,位上是1的不能放皇后;取反:1可以放皇后0不可以放;取与(&)把左侧位截掉
- int res=0;
- while (pos!=0){
- mostRightOne=pos&(~pos+1);//最右侧的1提取出来
- pos=pos-mostRightOne;
- res+=process2(limit,colLim|mostRightOne,(leftDiaLim|mostRightOne)<<1,(rightDiaLim|mostRightOne)>>1);
- }
- return res;
- }
1-10. 补充视频
1-10-1. Dijkstra算法优化改进
1-10-2. 暴力递归
暴力递归就是尝试,只要按照经验选择局部尝试得到结果
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题结果之后的决策过程
- 不记录每一个子问题的解
尝试是动态规划的基础。(将在提升班讲解),试法出来后可以搞动态规划。
1-10-2-1. 汉诺塔问题
打印N层汉诺塔从最左边移动到最右边的过程,最小步数
- public static void hanoi(int n){
- if(n>0){
- func(n,"左","右","中");
- }
- }
- private static 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);
- }
- }
1-10-2-2. 打印字符串的全部子序列,包括空串(就是所有子集)
- public static void printAllSubsquences(String str){
- char[] chs = str.toCharArray();
- process(chs,0);
- }
- // 当前来到i位置,要和不要,走两条路
- // 之前的选择,所形成的的结果,是string
- private static void process(char[] str, int i) {
- if(i==str.length){
- System.out.println(String.valueOf(str));//复用str空间,时间复杂度与function一样
- return;
- }
- process(str,i+1);
- char tmp=str[i];
- str[i]=0;
- process(str,i+1);
- str[i]=tmp;
- }
1-10-2-3. 打印字符串的全部排列(不要出现重复排列) 排列组合问题
- public static ArrayList<String> Permutation(String str){
- ArrayList<String> res=new ArrayList<>();
- if(str==null||str.length()==0){
- return res;
- }
- char[] chs = str.toCharArray();
- process(chs,0,res);
- // res.sort(null);
- return res;
- }
- // str[i...]范围上所有的字符,都可以在i位置上,后续都取尝试
- // str[0...i-1]范围上,是之前做的选择
- // 请把所有的字符串形成的全2排列,加入到res里去
- private static void process(char[] chs, int i, ArrayList<String> res) {
- if(i==chs.length){
- res.add(String.valueOf(chs));
- }
- boolean[] visit=new boolean[26];
- for (int j = 0; j < chs.length; j++) {
- if(!visit[chs[j]-'a']){
- visit[chs[j]-'a']=true;
- swap(chs,i,j);
- process(chs,i+1,res);
- swap(chs,i,j);
- }
- }
- }
1-10-2-4. 题目八:纸牌(先手|后手拿牌)
- public static int win1(int[] arr){
- if(arr==null||arr.length==0){
- return 0;
- }
- return Math.max(f(arr,0,arr.length-1),s(arr,0,arr.length-1));
- }
- private static 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));
- }
- private static 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-10-2-5. 逆序栈
给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数,如何实现?
- public static void reverse(Stack<Integer> stack){
- if(stack.isEmpty()){
- return;
- }
- int i=f(stack);//每次弹出结果
- reverse(stack);//递归完结果是逆序的
- stack.push(i);//按照逆序压入栈中,最终实现栈逆序
- }
- public static int f(Stack<Integer> stack) {
- int result=stack.pop();
- if(stack.isEmpty()){
- return result;
- }else{
- int last=f(stack);
- stack.push(result);
- return last;
- }
- }
1-10-2-6. 数字与字母转化
规定1和A对应,2和B对应,3和C对应。。。,那么一个数字字符串比如“111”就可以转化为“AAA”,“KA”和“AK”。给定一个只有数字字符组成的字符串str,返回多少种转化结果。
- public static int number(String str){
- if(str==null||str.length()==0){
- return 0;
- }
- return process(str.toCharArray(),0);
- }
- /*
- * i之前的位置如何转化已经做过决定了
- * i...有多少种转化的结果
- * */
- public static int process(char[] chs, int i) {
- if(i==chs.length){
- return 1;
- }
- if(chs[i]=='0'){
- return 0;//之前转后导致当前的0无法有效转化
- }
- if(chs[i]=='1'){//当前字符为1时
- int res=process(chs,i+1);//i 自己作为单独的部分,后续有多少种方法
- if(i+1<chs.length){
- res+=process(chs,i+2);//(i和(i+1)作为单独的部分,后续有多少种算法)
- }
- return res;
- }
- if(chs[i]=='2'){//当前字符为2时
- int res=process(chs,i+1);// i自己作为单独的部分,后续有多少种方法
- if(i+1<chs.length && (chs[i+1]>='0' && chs[i+1] <='6')){//0-6之间保证集合长度26
- res+=process(chs,i+2);
- }
- return res;
- }
- return process(chs,i+1);
- }
1-10-2-7. 重量和价值
给定两个长度为N的数组weights和values,weights[i]和values[i]分别代表i号货品的重量和价值,给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量,返回你能装下最多的价值是多少?
- public static int maxValue1(int[] weights,int[] values,int bag){
- return process1(weights,values,0,0,bag);
- }
- // i...的货物自由选择,形成的最大价值返回;从i开始做决定形成的最大价值
- // 重量永远不要超过bag
- // alreadyweight之前做的决定,所达到的重量
- public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {//可变参数少比较好,越容易改动态规划
- if(alreadyweight>bag){
- return 0;
- }
- if(i==weights.length){
- return 0;
- }
- return Math.max(process1(weights, values, i+1, alreadyweight, bag),
- values[i]+process1(weights, values, i+1, alreadyweight+weights[i], bag));
- }
尝试的代码暴力递归可以该着成位运算提高速度。