5.31开始的新笔记
一:数组
(1).解决思想:
解决数组算法问题时,可以依据一些核心思想来设计解决方案,这些思想能帮助简化问题,提高效率。以下是几个关键的核心思想:
1.双指针技术:(尤其适用于有序数组。)包括左右指针和快慢指针; 2.哈希表:将数据进行存储; 3.排序:在处理数组之前可以先对数组的数据进行排序,然后再进行处理; 4.其他的可以后面再说,相对复杂一点的方法,比如动态规划等等;
11.盛最多水的容器:
这个题的情况,上来的思想是用左右指针,谁小移动谁,只有移动小的,下一个结果才有可能比现在的结果大,我们要的是最大的结果; 步骤: 1.定义结果res,定义左右指针, 2.进行while循环,循环条件为右指针不等于左指针, 3.取两个指针的差值当做宽,和数值的最小值当做高,进行乘积计算,求最小值的时候可以用Math.min(左,右), 4.然后用三元表达式看一下结果与res大小对比,进行res的赋值;
283.移动零
用快慢指针的思想,需要注意最后一次循环的时候快指针也加一了,但是加完以后并没有任何操作; 步骤: 1.定义快慢指针,对快指针进行循环,当值不为0的时候,将慢指针赋值,然后慢指针也加一; 2.循环完毕以后,while循环把慢指针后面的值都赋值为0,循环条件为慢指针小于数组的长度;
206.翻转链表
需要唤起一些链表的基础知识:cur 指向当前的节点,方便作为每次循环的变量; 定义一个 temp去备份一下节点; 链表的最后一个节点的next指向null,可以用这个作判断看是不是遍历结束; 步骤: 1.定义一个前置指针pre=null,因为节点1一会要指向这个null节点,以后每次循环下一个节点都要指向pre节点, 定义一个当前节点,每次循环都是对当前节点做操作; 2.进行while循环,循环条件为当前节点是不是为空; 3.每次处理一个节点,切记这是翻转链表,只处理一个节点:备份当前节点的下一个节点,将当前节点的next指针指向pre; 只需要将当前节点next指向pre就行,只操作这一个节点; 4.变量复位: pre后移指向当前节点cur,cur指向备份的节点; 5.最后返回pre节点,因为最后一次循环,备份的节点就是那个null,所以while循环的时候才会条件不通过;
141.环形链表
使用快慢指针,循环条件为快的到头,如果链表不是环形的,肯定是快的先探到边界; 步骤: 1.定义一个快慢指针, 2.while true循环,快指针走两步,慢指针走一步,如果快指针或者是fast.next为null则返回false,如果相遇则返回true;
88.合并两个有序数组;
这道题可以得到的经验,快慢指针不仅仅是从第一个索引开始,如果有需要的话,也可以从最后一个索引开始; 另外这道题要考虑索引的边界问题,因为是从小到大排列,所以如果小的数组先走完的话,那大的数组就不需要再走了,就直接结束了, 因为最后的结果是按照顺序放在大的数组里的;如果是大的数组先走完,那下次循环就不需要对比了,直接让小数组自己走就行了,所以while循环应该以index2为主,只要index2走完了,那么index1就不需要再走了;(这道题可以参考提交的代码中的答案;) 步骤: 1.定义三个指针,都是在最右边,数组1,数组2,1和2之和, 2.while循环,循环条件就像上面说的那样,只要index2走完就可以了,index1是不需要再进行对比了,所以条件为index2>=0; 3.两个数组的值进行对比,取大的放在数组1的后面,然后让相对应的索引进行减一,总的索引也要进行减一;
66.加一
上面定义一个每次计算的加数进位变量carry,从最低位开始循环,让每一位与加数相加,然后取10的余数为这一位的值,取10的除数为向上一位进1的值, 最后循环完毕以后,看一下进位是不是1,如果是的话,说明这个数是999,则此时需要一个新的数组,数组长度比以前多1位,结果为1000; 步骤: 1.定义一个进位carry; 2.索引从大到小进行for循环,当前值加上carry对10取余数得到新值,取除数得到新的carry值; 3,循环完毕判断carry是不是大于0,如果是就建立新的数组,将第一个值置为1;
9.回文数
这里题目给的参数不是数组,而是一个 int值,这个题的重点在于知道怎么通过计算把一个数字进行翻转; 需要知道的是,一个数对10取余可以得到个位的值,除以10可以得到去掉最后一位的值,比如899除以10 等于89; 这样的话,余数就可以知道最后一位是9,然后对89再取余可以得到9,再取余得到8;那组合起来就是翻转以后的值了; 那么每个数字得到了,怎么得到合起来的值呢,可以将上一次的数字乘以10,然后加上这次的值,这次的值是个位; 什么时候循环结束呢,就是翻转之后的值大于或者等于了原值了,原值在不断的减少,翻转的在不断增大,大于等于说明已经翻转了一半了,可以结束了; 步骤: 1.进行特殊值处理,如果是负数,或者值不为0,但是对10取余数得0,则说明最后一位是0,直接返回false; 2.while循环,循环条件为小于原值, 3.进行上面说的逻辑处理,新值newInt=newInt*10+原始%10; 4.最后判断如果是两个值相等,或者是新的值除以10等于原值(除以10,说明多了一位),则返回false;
1.两数之和
这个题比较简单,关键是要注意不能使用重复元素,所以如果先把数据都放在map中,那有可能用到自己(这样不行),比如两个3就是6; 可以一边遍历一边把数据放在map中,这样只能让后面的数据使用前面的数据,防止出现用到自己的情况; 步骤: 1.定义一个缓存; 2.进行for循环,先判断缓存中是否有想要的值,如果有,可以直接返回了; 3.如果没有,则把当前值放在缓存中,让后面的去找我,因为我不能用自己;
15.三数之和
思路是进行两层循环,第一层从第一个开始遍历,第一个元素当做 target,然后第二层从第i+1个开始遍历,把第二层当成解决两数之和的题目; 以此类推,让第一层向后进行遍历; 遇到满足的,还需要向下遍历,把结果放进集合中,找到所有的结果; 这个题使用上面的缓存可以解决,但是要注意将里面的集合排序,外面的大集合要进行去重,这种方法要记得去重; 但是使用双指针的话,效率会更高; 步骤(嵌套两数之和的解法,能过测试,但是不是很快): 1.首先要对数组进行排序,这样当遍历到大于0的时候,说明后面已经不用遍历了,建一个set集合,用于存放结果去重; 2.然后从index为0开始进行遍历,遍历到倒数第三个数,因为现在遍历的是三个数中的第一个,后面起码还会有两个数; 3.进行剪枝,如果后面一个数与前一个相同,答案基本就是重复的,那可以直接进行下次循环了;如果这个索引的值,大于0了, 那么可以直接结束了,后面不可能成功了; 4.像两数之和一样,做一个缓存map,开始进行遍历,如果符合的话,就把结果放在list中,然后将list进行排序,放在set中, 方便去重,(这里记一下list排序的api,)如果不符合,就放在map中,进行下一次循环;就算是符合也要进行下一次循环,因为这次 答案有很多个,去重就交给set去做; 5.最后将set转化为list结束;
18.四数之和
主要是在三数之和解法的外面套一层循环,也要注意遇到相同的数的话,要合理的跳过本轮循环,另外注意最后的去重操作的那两行代码,确保熟练;
LCP 18. 早餐组合
对两个数组进行排序以后,使用双指针从左右两个方向进行while循环; 先对两个数组进行排序, 遍历以主食为主,让主食从小到大进行遍历,每次遍历的时候都拿饮料价格最高的,因为如果最高的已经满足的话,说明下面的都满足了,把坐标加一下就行,如果当前的饮料价格已经不满足了,那么主食再往后遍历都是越来越大的,肯定就不会满足了,所以就不需要管主食右边的价格了,肯定都是不满足的; 注意:双指针最好都是用的一个循环,一层循环; 步骤: 1.将两个数组进行排序,定义左右指针,左指针从食物最小的坐标开始,右指针从饮料最大的开始,再定义一个变量存储方案的个数; 2.进行while循环,循环条件为左右指针都没有走到尽头; 3.如果组合满足条件,说明这种食物和右指针左侧的所有饮料都满足,那就把组合数进行相加就行; 4.如果不满足,说明左指针右边的食物结合这种饮料都不会满足,因为右边的食物价格会更高,这种饮料以后可以不用考虑了,那就把饮料 价格降低,即饮料指针左移; 5.返回组合数就行,记得进行取余,不要太大; 二刷: 1.记得进行排序,定义左右指针,在一个while循环里(条件是两个都不结束),当这一轮的主食价格与饮料价格匹配以后,将个数加到总和里,然后开始下一轮的主食,如果饮料价格太大了,不满足这一轮了,那就减少饮料价格,因为主食是越来越大的,如果这一轮不满足,那这个价格的饮料以后就不用试了;
26.删除有序数组中的重复项
比较简单,定义一个快慢指针,快指针进行总的循环,如果遇到与慢指针的值一样的,则直接跳过,进行下一次,最后取慢指针的索引加一即可; 二刷:定义一个快慢指针,让快指针从1走到头,因为0是要占用的,不需要考虑,当这个索引的值与上一个不同的时候,才让慢指针指向这个索引,慢指针的索引加一, 不管什么情况,每次循环的时候,快指针的索引都会加一;
189.轮转数组
如果是轮转3,则相当于是原始的索引加3,然后对长度取余就是新的索引,用一个新的数组去存这些值,最后进行拷贝,这里主要记得拷贝的api就行,别的不难; System.arraycopy(newNums,0,nums,0,nums.length);
二:链表
解决思想:
把链表的每个节点想象成这个值的代表就行,不用管指针,以值为主;
141.环形链表
使用快慢指针,如果慢指针能够追上快指针的话,说明就有环; 步骤: 1.定义一个快慢指针, 2.while true循环,快指针走两步,慢指针走一步,如果快指针或者是fast.next为null则返回false,如果相遇则返回true;
142.环形链表 II
上一题是基础版本,可以得到两个指针相遇时候的点,但是这个点不一定是在入口,只是在环里,而且不知道是在环里走了多少圈才得到的; 所以解决这个问题的时候,可以画一个图,设环前的长度为a,环的长度为b,得到一个等式,利用数学解出来 让快指针从头走a,慢指针从 相遇点走 a就可以得到入口; 步骤; 1.定义一个快慢指针; 2.进行while true循环,快指针走两步,慢指针走一步,如果fast==null,或者是fast.next==null,则返回false,说明没有环; 如果相遇了,则说明有环了,此时也知道了相遇的地点; 3.让fast=head,再从头走a步,就能得到入口的位置,代码表示就是fast=head,while循环,条件为fast!=slow,一直走,直到相遇;
2.两数相加
第一是循环的判断条件是两个链表只要有一个不是空就可以进行循环,另一个是空的数值当成0即可; 第二是,这里需要使用一个哨兵模式,记录每次的结果,最后返回一个哨兵的next就行; 最后记得再判断一下 carry的值;(链表从前到后是从个位到高位的) 步骤: 1.进行特殊判断,如果一个为空就返回另一个就好了,构建进位和哨兵; 2.进行while循环,条件是l1、l2只要一个不为null,因为null的话,值可以用0代替; 3.得到l1、l2本次循环的值,如果是null,则为0,然后计算余数和carry进位,然后将两个链表和结果像后移动,进行循环; 4.循环完毕看一下 carry是不是大于0,是的话就再多构建一位,最后返回哨兵的next节点就行; 二刷:主要注意一下哨兵节点的写法,链表的遍历;
21.合并两个有序链表
定义一个哨兵,进行循环,循环条件是两个都不能为空,对比谁小就把谁的值放在哨兵的next节点,如果有其中的一个遍历完了,则哨兵的next节点就直接指向另一个链表,就结束了; 步骤: 1.进行特殊判断,如果有一个为空,则直接返回另一个;定义一个哨兵节点作为res链表; 2.进行while循环,条件为两个链表都不为空,对值进行比较,值小的放在res链表的后面,然后让l1=l1.next,res=res.next,进行下次 循环,直到有一个循环完了,然后让另一个链表挂到res上就行; 3.最后返回哨兵节点.next;
24.两两交换链表中的节点
这个题在写的时候一把就过了,下面总结一下心得: 链表题大部分主要就是玩指针,画图看一下指针的指向变化; 写代码的时候注意,上面的变量如果重新给定义(改变)了,那么下面引用的时候是引用的新的,如果想用原来的位置,那么就需要调整一下代码顺序; 技巧: 在定义变量的时候,为了自己能看的简单,可以根据图中的代号去定义变量的名称,比如定义变量用 l1 l2 l3 l4去代表图形中对应位置的值; 这个技巧哦不一定好用,还是用cur 或者是start end吧,下一轮开始就用 nextstart) 步骤: 1.进行特殊判断,如果链表为空,或者只有一个节点,则直接返回;然后定义哨兵节点,也作为pre节点; 2.进行while循环,条件是头节点不是null,head.next不是null,只要小于两个就说明循环要结束了; 3.找到第二个,和第三个节点, 然后进行指针处理:2.next=head;2指向1,1指向3,pre指向1; (到时候画一个图,从后往前找,l1->l3,再l2->l1,再pre->l2,从后往前指向,按照这个顺序,到时候不要手忙脚乱) 4.上面指针处理完以后进行复位:pre指向1,新的1就是head指向3;进行下次循环;
25.K 个一组翻转链表
解决方法应该与上面相同,只不过是中间多调用一个翻转链表的方法; 在找下一组的开头的时候, 上一题可以用 cur.next.next直接找到,K个一组的话,通过循环K次去就可以得到下一组的头节点;找到头节点以后,整体的操作可以用上面的思路是一致的; 问题:翻转链表的代码不是很熟练,这种应该是一分钟写出来的,结果耽误了很久。另外在定义变量的时候,用的代称不是很好,导致有点逻辑混乱 这个题要多写几次,比较综合,特别是一些常用的简单代码,应该达到背会的程度,多进行记忆; 步骤: 1.定义一个哨兵节点,dump.next=head; 然后定义两个指针,pre和end,end是为了走k步用的,也就是一个组的末尾,方便循环使用; 2.while循环,循环条件是end!=null,也就是链表没有走完,还可以向下走; 3.让end走k步找到一组的尾结点, 这里需要注意的是for循环的判断条件是两个 i<k&&end!=null; 判断一下,如果end==null了,就 是不够k个了,那直接结束循环; 4.开始正式的操作,既然要循环就要找到一组的开始节点和结束节点,让结束节点.next=null; 这之前要不结束节点.next进行备份 bak节点; 找到这两个节点以后开始进行翻转,翻转完以后尾结点成了头节点,头节点到了尾结点 5.这时候需要将一组与主链想链接,怎么链接呢,让pre.next=end,start.next=bak节点;因为翻转以后尾结点end成了头,start成了 尾部,所以这样进行连接; 6.连接以后,需要对变量进行复位,主要用到了两个变量,就是一开始定义的那两个,这里理解了,每次复位都是复位一开始定义的变量, 所以需要复位pre和end,开始定义的时候是在什么位置,那复位就是在什么位置,显然,定义的是都在每个小组的前面,也就是上一个小 组的结束的位置; 注:步骤中翻转链表的步骤,看上面的题目;
三:栈
注:deque比stack要快,因为stack是继承自vector,vector是线程安全的,速度上会更低一点,很多地方都用synchronize修饰了,所以效率不高, 而deque是可以转成线程安全的
解决思想:
1. 熟悉栈的基本操作 压栈(push):将元素添加到栈顶。 弹栈(pop):移除栈顶元素。 查看栈顶元素(peek):获取栈顶元素但不移除它。 检查栈是否为空(isEmpty):判断栈是否为空。 2. 了解常见的栈应用场景 2.1. 括号匹配问题 2.2. 单调栈 单调栈用于解决涉及下一个或前一个更大/更小元素的问题,例如:每日温度(Daily Temperatures)、下一个更大元素(Next Greater Element)。 2.3. 栈排序 利用两个栈对一个栈进行排序。 2.4. 用栈实现其他数据结构 如前面提到的用栈实现队列,或者用两个栈实现最小栈。
工具1.单调栈
1.解决的问题: (1)寻找每个元素左边第一个比它小(或大)的元素; (2)寻找每个元素右边第一个比它小(或大)的元素; (3)滑动窗口最大值问题; (4)直方图中的最大矩形面积; (5)股票跨度问题; (6)下一个更大元素问题; (7)每日温度,接雨水,去除重复字母,股票价格跨度,移调K位数字,最短无序连续子数组; 2.单调栈的逻辑叙述: 单调递减栈:栈内元素是降序排列的,比如 3 4 12 ,它能找到左边第一个比它小的元素,当有新的元素插进来的时候,如果需要栈顶元素 弹出,则说明刚刚插入的这个元素 是栈顶元素右边的第一个比它小的,比如 12后面插进来一个 7,这时候就可以得到 12 的左右两边第一个比它小的数;如果左边的元素是重复的,比如左边有好几个4,怎么确定是哪一个4呢, 方法是数组存入栈 的时候存的是下标,对比的时候比的是值,但是存的时候存的是下标; 单调递增栈:与上面相同,顺序相反; 3.步骤: (1)定义一个数组,用于存储结果,定义一个栈,可以用java里的双端对列实现; (2)将参数数组进行for循环,找到每一个数左边第一个比它小的数; (3)这个数要入栈,这是一个单调递减栈,从大到小进行排序的,如果入栈的元素比栈顶元素小,那需要栈顶元素进行弹出,才能存放入栈的元素,至于弹出几个,也不知道,需要一个一个进行对比,直到存放的元素大于栈顶元素为止。 所以就需要一个while循环,判断条件是如果栈不为空,就一直判断栈顶元素是不是大于等于存放的元素,如果是就将栈顶弹出,直到栈顶元素比它小。 (4)对比完成以后,到了可以入栈的时候了,前面已经没有比它大或者跟它相等的了,那此时的栈顶元素就是左边第一个比它小的,那就记录一下栈顶这个值,如果这时候栈顶是空的,那就记录一下默认值;记录完以后直接入栈就行; 4.步骤的改进版本: 就是加了一个哨兵,因为上面的标准步骤,每一步都要进行判空,加了哨兵以后保证栈里一直有值,不需要进行判空操作,那不是说如果没有比它小的元素就写成-1吗,怎么办: 那就把栈的哨兵写成-1就行,这样刚好就是默认值了; 5.用单调栈解决的问题:直接跳到84题;
-
有效的括号
刚刚做了一遍,写一下总结:为了缩短时间,思路没有完全整理好就开始写了,所以有一些小问题;另外就是api根本不熟,不知道就能干嘛,所以也有一些问题; 步骤: 1.特殊判断,如果是空,可以直接返回,然后判断字符串的长度是不是偶数,如果不是直接返回false;这里需要注意,如果是用string的话,length是一个方法,不是属性,所以要加括号; 2.将字符串转化成数组,然后新建一个栈,做准备工作; 3.对数组进行循环,如果是左括号,就把右括号放在栈里,如果是右括号,就看是不是等于栈里第一个元素,如果不是,就返回false,这里注意,如果栈里为空,则说明也有问题,就是右括号在前,也是不对的,直接返回false; 4.最后结束以后看栈里是不是已经空了,如果已经空了,则说明是true,如果不是空,则说明有左括号没有处理完,结果还是false;
155.最小栈
用两个栈去实现,一个栈存正常的所有的元素,一个栈存最小元素; push:存入正常栈,如果存入的元素比最小栈的栈顶元素小,或者相等的话,就存入最小栈一份,当然了,如果最小栈是空的话,肯定要存一份;相等也要存,因为这次最小值弹出去了,那下次最小值还是他,不存的话,弹出去一次就没有了; pop:正常栈就正常操作,如果弹出的这个元素等于最小栈的栈顶元素大小,则最小栈也要弹出的,因为这个元素在栈中没有了,两个都要弹出,以后不再操作这个元素了; top:按照要求直接返回正常的栈顶元素就行; getMin:返回最小栈的栈顶元素就行; 步骤: 也没有什么过程,就是单纯熟悉一下api;
232.用栈实现队列-C
解析:题目说了用两个栈实现这个队列,那基本思路就已经定下来了,怎么实现先进先出呢,栈明明是后进先出的,从这里考虑,只能说让第二个栈的元素入栈顺序跟我们push的顺序是相反的,怎么相反呢,那就把第一个栈的元素全部倒出来,因为倒的顺序就是反的,这样一来算是负负得正了。所以什么时候倒呢,倒一次以后这段时间肯定不用再倒了,再倒就乱了顺序了,那下次什么时候倒呢,栈为空的时候才能倒,所以这个条件就出来了; 那什么时候需要判断空呢,就是需要弹出的时候吧,需要弹出元素的时候判断空,或者是获取队列头元素的时候; 步骤: 没啥说的,就是在需要pop或者peek的时候,判断一下second有没有,如果为空的话,就用while循环,将first的全部给倒入到second中;
84.柱状图中最大的矩形-C
先学习了单调递减栈,也学习了代码的实现,可以得到结论是,入栈的时候让我弹出去的那个数肯定是右边第一个比我小的,栈里我左边存的那个数是左边第一个比我小的,当我弹出的时候,看一下谁是栈顶就知道我左边第一个小的是谁。问题是怎么找到下标呢,我们要的应该是下标; 右边好找,因为刚好遍历到这个数,那左边呢,不能根据数值去找下标吧,比如放在map里,因为数值有可能是重复的,所以没法用map缓存,那怎么办呢: 我们在栈里存的时候就存下标,既然是数组,每次对比的时候拿到下标不就有值了,就可以对比值了,对比完以后存的还是下标,下标多么好的解决方法,我开始还在疑惑,这个单调递减栈怎么解决这个问题呢,他用下标给解决了,下标还在前面解决过其他问题,以后可以多考虑一下下标在解题中有没有用处。 如果有的元素入栈了以后,一直没有比它小的让他出来怎么办,那就不能完全遍历了,他给的方法是,最后像哨兵一样,再给他一个最小的数,能把栈里的数全部都倒出来; 注:这个跟单调栈的重心不一样,单调栈的重心在每个元素入栈的时候去做处理, 这个的重心是在每个元素弹出的时候去做处理,弹出的时候知道新元素是他右边第一个小的,弹出以后的栈顶元素是他左边第一个比它小的,这下就能算出来面积了,为了防止左边元素重复,所以存的时候存的也是坐标值; 还有需要注意的点是:这里的哨兵是两个,第一个是方便找到左边界,不然每次得判空;第二个是末尾为了让栈中的元素全部倒出用的,因为最后一个元素不一定是最小元素,如果不是最小元素,则按照单调栈的规则,里面肯定还会有其他元素没有出来,那我们的规则是以弹出的这个元素为中心进行操作,所以如果没弹出来的话,肯定是不能操作的,那最后就需要一个很小值的哨兵把栈里面的所有元素都给倒出来,让所有元素都弹出来; 步骤: 1.定义一个新的数组,然后第一个值和最后一个值是-1,然后将原数组拷贝到这个数组里;定义一个栈,第一个元素直接存好,不参与循环了,定义一个int变量存储最大面积;(这里注意一下拷贝数组的api) 2.进行for循环,将每一个元素与栈顶元素对比,看用不用弹出来,如果需要弹出来,就进行上面的逻辑:当前元素就是右边第一个小的,那就是右边界,弹出来以后的栈顶元素就是 左边第一个小的,就是左边界,高度就是弹出来的这个下标的value值,然后计算一下面积。 3.将这次的值添加到栈中;
239.滑动窗口最大值
这是找K个里面的最大值,单调递 减队列的队列头是这些数据里的最大值,至于为什么用单调递减队列,以后经验多了可以总结一下。 这里面用单调递减队列,就是从队列的头到尾是递减的,队列头是最大元素,在画图的时候,大家都习惯于将左边作为对列头,然后由右边向左添加元素,既然单调递减队列是从头到尾部递减的,那尾部应该是最小的才能放进去,如果新元素大于队列的尾部,需要将尾部的元素弹出来,直到新元素小于对列尾部的元素,才可以入队列,所以这里有一个while循环进行判断; 理解他的思想,有数据进来的时候,会根据单调递减队列的特点,进行比对,如果新的数据大的话,就要让之前的出队,然后新数据入队,最大的在队列头; 问题是窗口往下走的时候,如果一开始的数据很大(叫第一个),那后面无论插入多少元素,第一个数据都不会被弹出,但是第一个数据已经不在窗口里了,这时候找最大值,找到就是第一个数据,这显然是不合适的,怎么解决; 解决:跟栈的解决方法一样,存的时候存的都是下标,这次得到的(第一个数据)是最大值,那计算一下下标的差值是不是大于窗口长度就行,如果大于就弹出去,由于循环到每个元素都需要对比,所以最多只会多存一个元素,不需要用while循环对比弹出,每次对比一下就行了,如果大于窗口长度就弹出去了; 步骤: 1.进行特殊判断,定义一些需要上下文使用的变量,返回的数组,这里注意一下数组的长度,还有数组的下标,后面下标要每次加一;既然这里思路是用单调递增队列,那么就需要定义一个队列,以后栈和队列熟悉的话,都用deque; 2.进行for循环,将数组中的每个元素都进行循环; 3.既然是单调递增队列,那么需要判断新元素是不是比对列尾部的元素大,如果是的话,需要进行while循环,将尾部的元素弹出来,直到新元素比对列尾部元素小,(等于的时候弹不弹都行,因为这里取得是窗口里的最大值,不是每一个的); 4.等可以入队列的时候,就把当前元素给入队列; 5.从窗口的最后一个元素开始,需要保存窗口的最大值到结果数组中。 最大值就是对列头元素,这时候需要判断这个最大值和当前元素是不是已经不在一个窗口了,如果不在的话,需要弹出去了,每次循环都进行了判断,所以对列中的元素不会进行积攒,所以不需要循环,每次只需要判断一次, 为了这一步,队列里就得保存下标了,数组的好处就在这,保存下标的话,既可以有值也可以有下标; 6.判断完以后,如果距离大于窗口了,就弹出去一个,不大就不管。然后将对列头元素存入结果数组中,让数组下标后移一位; 7.返回结果数组就行;
四:hash
总结:
1.解决hash冲突主要有开放寻址法 和拉链法;
242.有效的字母异位词
分析:这个题主要是可以设计一种hash函数,能够映射到数组的下标。比较简单,但是写一下,里面有一些api是新的,比如string.charAt(i)返回这个索引的字母,还有字母减去 'a'得到下标的方式,都是新的。 步骤: 1.进行特殊判断;如果长度不相同,就直接返回false; 2.构建数组,遍历第一个字符串,在对应的位置进行++,(每次都要算位置,可以抽取成函数) 3.遍历数组,在对应的位置--,如果value小于0,直接返回false;因为前面已经判断了长度相同,所以不存在所有的值都不小于0,还会有值大于0的情况,所以不需要再遍历判断有没有值大于0了;
49. 字母异位词分组
分析: 也算是比较简单,既然分组,肯定有key,value是一个list集合,这样显然就是用map装一下结果;这个好理解,那key用什么表示呢,异位词怎么得到相同的key呢,可以根据上一题的,做一个数组出来,有数据的索引值进行++,最后得到的数组是一样的,然后将所有有数据的索引包括值进行组合成一个字符串作为key,1-2-5-1-6-1这样,当然了-可以少一点,只是演示用的,这样如果key相同,则把这个词放在map对应的value中,value是一个list集合,将词加入list集合就行了; 步骤: 1.进行特殊判断,如果为空,或者长度=0,则直接返回一个空的list; 2.定义一个map,用于保存结果,key为字符串,value为list集合;将字符串数组进行遍历; 3.根据字符串数组转化成key,转化的方法就是上面写的,跟上一题也一样,一个int数组,两个for循环,第一个循环进行++,第二个循环将有大于0的进行组合,通过stringbuilder的append方法,最后调用toString转成字符串,这就得到了key; 4.遍历的时候map.containsKey 看看有没有刚刚生成的key,如果有的话就直接将这个词添加到value的list中,如果没有的话,新建一个list,然后将这个词添加进去; 5.最后返回一下map.values();将他们放进一个新建的list中,new ArrayList(map.values());
五:递归
总结:
先写一部分,后面随着总结的增多,再一点点往上加; 1.确定递归的出口; 2.确定本层要做什么,模型思考一个树形结构,从left节点一直向下走,然后回溯到上一个节点;
22.括号生成
分析:需要递归和回溯,其实可以放在后面去做,看了答案以后也不是很难,但是这个剪枝自己想的话,就很困难;
五.1递归之分治回溯:
总: 1.回溯在哪里:递归函数下面的代码逻辑就是回溯的逻辑,属于暴力搜索的范畴。 2.回溯能解决什么问题:组合问题,切割问题(字符串),子集问题,排列问题(与组合比,有顺序),棋盘问题。 3.回溯怎么抽象:回溯问题都可以抽象为上图中(代码随想录中的图形)的树形结构,同层的使用for循环进行处理,往下的使用递归进行处理。 4.终止条件的时候收集结果。for循环里面先处理节点,再递归函数,再回溯撤销掉之前的处理; 1.对于二叉树,写一下从下往上遍历的方法: 二叉树只能从上往下遍历,有的时候需要从下往上找,怎么找呢,可以通过回溯,就是当下层的递归把结果返回回来以后,这一层处理结果就对应了从下往上遍历,对应着后续遍历,就是先处理左右子树,然后处理中间节点。。后面遇到题再总结。 2.关于回溯算法: 回溯算法有点深度优先遍历的意思,每次探到底部,得到结果后向上返回,开始探别的方案,每次都先走到最深。
77.组合
思考: 如果没有回溯的话,这种问题可以用for循环解决,求两个数的组合,可以用两层for循环,但是这里是求k个数的组合,就没法用for循环了。 对于组合,是不讲究选出来的元素的顺序的,所以元素的组合是不重复的,如果前面已经找了,遍历到后面的时候就不需要往前找元素了,因为前面的已经找过后面得了,如果后面的再去找前面的,那肯定就重复了。 还有一个需要注意的是,既然现在是每一层只往后面找了,那怎么知道从哪个index开始呢,比起排列就多需要了一个元素,就是for循环的起始索引,startindex。不能使用排列里面的level,因为比如图中的,第一层选的3,那第二层只能从4开始找,所以不是像level一样,单纯的表示第几层,而是需要表示下一层从哪个索引开始找,也就是本层遍历到的索引值i+1; 思路:n代表了1到n,就是一个数组,for循环的时候,从1开始到n就行。终止条件以及层级逻辑都比较好写,问题是层级逻辑在进入下一层的时候,进入下一次递归开始,for循环不是从起始位置1开始的了。比如根据图中,到第二层的时候第一个分支,1已经选过了,第二层从2开始选;第二个分支,2已经选过了,第二层从3开始选,对于组合问题,为了避免重复,只选后面的,不往前选,所以起始位置其实是上一层选择的index值加一。 步骤: 1.写一些全局变量,存储结果,存储路径,还有一个递归的参数是这次循环的起始索引。 2.写终止条件,如果路径的size已经等于k了,那可以保存结果然后返回。如果起始索引startIndex大于n了,比如图中第一层选的是4,下一层的时候startIndex=5,大于n了,没法再选择了,所以也终止; 3.层级逻辑:对每一层进行for循环,循环的起点就是参数中传的startIndex,选中加入到路径中,然后进入下一层递归,进入的时候将下一层的startIndex传进去,就是上面说的i+1,下一层返回以后开始回溯撤销。 补充:说一下我自己剪枝找边界的方法 path.size() : 已经找的个数 k-path.size() :还需找的个数 【x, n】的数组长度起码应该是k-path.size()才有继续搜索的可能, 那么就有 n-x+1 = k-path.size() , 解方程得 x = n+1 - (k-path.size()), 而且这个x是可以作为起点往下搜的 也就是for(i = s; i<=x; i++) 这里的x是可以取到的
补充:剪枝 对于上面的图,如果k=3,那么遍历到第一层的时候从3开始已经不需要处理了,因为后面已经不可能了,这对于图中数据量较小的情况,看起来好像多花不了多长时间,但是如果数据量很大的时候,每一个分支都很深,那就多消耗了很多资源,所以要进行剪枝。什么时候就不需要进行遍历了呢,比如3分支,加上3一共就剩2个数了,就没必要了,怎么算呢,就是path中已经存的个数,加上剩余的数的个数依旧小于k,就不需要进行操作了。 但是在回溯中,剪枝一般放在for循环的i的范围上,所以等式算完以后要放在for循环的i的范围部分,path.size+(n-i)>=k,通过等式算出来i的范围。
二刷思路: 脑子里构思一个树的结构,这只能从前往后找,避免重复,所以需要一个startindex。 其他的需要存储结果的list,存储路径的栈,这三个变量。 过程是,如果路径size==k了,那就说明满足了,放进list结果集合中,这是递归的出口。 然后开始遍历,从startindex开始,到结尾,选i然后递归到下一层,出来以后把i弹出来。 最后是剪枝,剪枝的思路看上面的就行;
236.二叉树的最近公共祖先
解题思路: 下次复习到这题的时候,可以用b站代码随想录的那个图,看着图思考一下。这里如果可以从下往上遍历可能会更好,回溯可以从下往上去处理结果,就是当下层的递归把结果返回回来以后,这一层处理结果就对应了从下往上遍历,对应着后续遍历,就是先处理左右子树,然后处理中间节点。 这个题,如果pq同时在左右子树,则遍历左右子树的时候找到了pq,则告诉父节点说pq就在它的左右子树上,这样就找到了它的最近公共祖先,然后将这个找到的节点一层一层向上返回。写这种题主要是将思路给建模抽象出来,知道了怎么回事,还是需要写成代码语言去表示的。 如果pq在root节点的左右两个子节点(比如pq是5和6,的最近公共祖先7),则递归回溯的时候告诉root节点,root节点就知道它是最近公共祖先,然后再一层一层告诉上面,就是返回给上面。那怎么保证一层一层返回以后一直是刚刚那个root节点呢,就是返回值保持不变,这个一会再说。 再往下,第二种情况,如果pq在一个root节点的左右两侧很多层的叶子节点,比如上面的图,如果没有的话就是随想录那个图,比如是5和20,就是5和20一层一层的向上返回,然后返回给8的时候,8的左右两个叶子节点的回溯都是有值的,这时候就跟上面的情况是一样的,8知道它是pq的最近公共祖先,条件一样,都是这一层递归的左右子树递归返回值都不是空。 第三种情况,p是7,q是5,此时p就是两个节点的最近公共祖先,这时候探测到7就不会向下探测了,那怎么保证一层层向上传递的时候还是7呢,这是就一个返回值不为空的情况,这种情况就还一层层向上传递,保证这个值不丢不换。就是本层的递归来说,如果左右子树的返回值中,只有一个不是空,则就还向上一层返回这个值。其实像第二种情况,5向上传递的时候也是需要一层层交给上面,保证这个值不丢,到8的时候才能会和。 总结下来,使用后续遍历,如果是单个有值的话,就向上传递,如果是两个都有值的话就返回本节点,如果两个都没值就返回null; 步骤: 1.写递归终止条件,每次遇到叶子节点的下一层的时候返回,就是当前的root==null的时候返回,遇到pq的时候也不需要向下遍历了,也返回; 2.本层的处理逻辑,采用后序遍历,从左右子树向下递归,先写左右子树的递归。 3.左右中,左右处理完了,中间逻辑是根据左右子树的返回值,如果两个都是空则返回空,如果有一个不是空则直接向上传递这个节点,如果两个都不是空,则返回本层root节点;
50.Pow(x, n)
思路: 题目比较简单,终止条件也比较好确定,就是当n=1或者0的时候,可以返回x或者1,可以得到终止条件。每一层的逻辑是类似后序遍历,先向下递归得到结果,然后将两个结果进行合并,这里需要注意的是,n可能为奇数,就不能平分,需要判断奇数和偶数,最好用一个变量存储一下递归的结果,这样可以少算一次,因为两个递归的参数是一样的,结果也是一样的。其他的需要注意的是n可能为负数,那就需要将x=1/x,通过这样把n变成正数,另外还需要做特殊值判断,n如果为负的integer的最大值,那么变成正数的时候就超出边界了,所以做一个特殊判断,如果等于的话,就让x=x的平方,这样n=n/2; 步骤: 1.做特殊值判断,如果是int的最小值,则需要将x=x平方,n=n/2;如果n为负数,则让x=1/x,n=-n转为正数; 2.开始递归,终止条件是当n=0或者n=1的时候返回1或者x; 3.层级处理逻辑:向下层递归,返回一个half值,然后根据n是奇数还是偶数进行判断,去处理这一层的结果,如果是奇数就是half*half*x,如果是偶数就是half*half;
46.全排列
思路: 可能是对回溯做的有点感觉了,题目本身觉得没什么难的,需要注意的是题目中对几个参数的解决,回溯肯定是要记录路径的,而且这一层探测完以后需要将刚刚记录的那个数给删掉,对于这种先进先出的,栈肯定是最合适的。还要记录已经选择过的数,那就需要一个和原数组等长的boolean数组,选择过以后记录为true,递归探出来以后还原的时候再改为false。还需要判断递归终点,这时候就需要标记一下探测到第几层了,所以需要一个level; 每次递归进来以后就要开始选择这些数据了,有可能是先选3,然后是1,2。这样就需要每次从头开始遍历数组,遍历的时候怎么知道这个有没有被选择过呢,可以通过boolean数组判断,如果没有的话就可以进行选择,向下一层递归,递归返回以后记得将选择标记和记录的路径复原就行; 步骤: 1.定义所需要的结果变量,路径deque变量,选择标记boolean数组,开始调用递归函数; 2.写递归的终止条件:如果层级大于数组的长度了,说明数组已经被选完了,这一次不能选了,则记录结果然后返回。 3.层级处理逻辑:不知道哪个数还没选,则遍历整个数组,然后根据boolean标记数组去判断有没有选择,如果没有则选择放入路径,将标记数组置为true。然后向下一层递归,递归上来以后将刚刚的标记还原。
二刷:全排列的结果是有顺序的,所以不是 只能从它现在的位置往后找,也可以往前找,那也就不需要这个startindex了,怎么判断哪个找过哪个没有找过呢,这就需要记录一下used[]数组了, 递归的出口是路径的size==nums.length
40.组合总和2
思考: 1.去重的时候怎么区分是树枝重复还是树层重复?(数组已经进行排序) 对于出现重复元素的时候,如果是一层的话,说明前一个元素我没选(上一轮循环已经选过它了,又重置了,这一轮到选我了,因为我跟他相等,所以就没必要选我了,因为这种可能性上一轮已经处理完了),所以标记数组used[i-1]是false;如果是树枝的话,说明上一个元素我已经选了,此时used[i-1]是true。 思路: 对于组合问题的思考方式肯定是与排列有区别的,但是只写了这一个,暂时还不能详细叙述区别。 对于组合而言,我选1,2,3三个元素与选2,1,3三个元素是一样的,但是对于排列来说是两个不同的结果。所以组合在递归的时候,每一层往下递归的时候,for循环开始的起点是不同的,这个位置的数选完以后,下一层就没必要选这个位置了,选了也是重复,所以下层循环是 对于本题思路需要先对数组进行排序,方便下面的去重操作。递归需要的参数跟排列差不多,一个装结果集的参数,一个路径,路径还是用栈表示,方便弹出去进行复原,一个level记录这一层遍历到第几个数了,这里level还有其他作用,因为每一层的遍历现在不从0开始了,从level开始, 这个题既然让算sum,所以还需要带上sum参数。终止条件是两个,一个是遍历结束了,level大于数组长度,另一个是sum刚好相等了,或者是大于了,大于也要结束,没必要继续了。 每一层遍历的时候,进行for循环,往后找,为什么不能找前面的呢,因为前面的已经找过后面的了,这种可能性已经计算了,如果再从后面找前面,那再找到刚刚那个元素,这种组合就重复了,所以只需要往后找就行了。
216.组合总和3
经过前面两个题,这个已经变得很简单了,找到终止条件就行,再加上适当的剪枝操作; 思路: 整体框架与上面一致,但是传参的时候需要带上和sum,因为终止条件需要判断如果path的size是k,并且和是n的话,可以加入结果集中。其他的终止条件是如果path.size已经>=k了,但是sum不是n,或者是sum已经是>=n了,但是size不是k,这种情况都要终止递归。 层级处理也跟固定的框架一致,多了一个sum求和的代码,撤销回溯的时候也要把sum给回溯了。 步骤: 1.定义参数的结果集,定义路径栈,参数中有常规的 startIndex,有题中需要的sum和; 2.写终止条件,上面思路中写的; 3.层级处理,for循环从startIndex开始,路径加到path,sum和加上,进行下一层递归,递归出来以后,撤销路径,撤销和;
六:二叉树
总结:
写完题目以后再总结吧以后;
144.二叉树的前序遍历
分析:以后写递归主要看好我的终止条件是什么,具体的一层是在干什么,这两个很重要,另外就是参数的起点要做一个计算;注意回溯的过程,这个过程在后面回溯的时候再总结; 题目中是前序遍历二叉树,就是先记录中间节点,再记录左子结点再记录右子节点,那每一层就是先记录根节点,然后开始像左子树走,让左子树进行一个前序遍历,以此类推;所以基本可以得到每一层要做的事情就是记录当前节点,然后向左子树递归,然后向右子树递归; 那递归的出口是啥,想象一下树的结构,每次递归的结束都是到叶子节点,就是当前节点的next节点是null;那剩下的就是一个保存每一层结果的参数,跟着每一层的调用向下传递; 步骤: 1.定义一个存储结果的list,调用recur递归函数; 2.设置终止条件,当前节点==null,直接返回; 3.当前步骤:记录当前节点,向左递归,向右递归;
590 .N叉树的后序遍历
很简单的一个题,为什么写这个呢:主要是想说一点,递归的结束不一定是自己写的那个出口,比如上面的当 root==null的时候直接return,这是递归的出口;还有什么是出口呢,就是这个方法真的执行完了,也会返回,比如这个题的,方法每层都是遍历所有的孩子节点,判断一下孩子节点是不是空,如果不是的话,就遍历孩子节点。那如果是空呢,是不是这个方法就执行完了,结束了,那这一层递归也结束了,所以也算是一个出口,而且是重要的出口。 步骤: 1.定义一个存储结果的list,调用recur递归函数; 2.当前节点==null,直接返回,这里不是终止条件,其实是为了判断特殊情况,防止测试用例传过来的是一个空节点,这一步可以放在递归外面; 3.判断当前节点的孩子节点集合是不是空集合,如果不是的话,就遍历所有的孩子节点,向下递归; 4.如果是空呢,没有孩子节点,说明当前节点已经是叶子节点,记录当前节点的值(因为是后续遍历,是先记录孩子节点再记录自己),这个递归深度到此就结束了;
101.对称二叉树
这一题有点感悟: 1.递归就像数学归纳法,其实不用人肉的去想每一层怎么走的,脑子往下想几层,想明白了才觉得自己懂了。其实写数学的时候也没这样去做归纳法的题目,不需要连着往下想好几层才能算是归纳的成立; 所以重点在于n=1的时候是怎样的,然后n=k的时候的逻辑要处理清除,其他的就交给程序了; 对于这道题而言,当n=1时就是,节点的左右子节点全部为null,或者是左右子节点的值相同,才是对称的,所以出口找到了。那当n=k的时候呢,要左子树和右子树的值是相同的,(这里因为不是末尾,所以不能因为值相同就返回true),并且左子树的左子结点与右子树的右子节点是对称的,并且左子树的右子结点与右子树的左子节点是对称的,好了,知道一层就可以了,不用再往下深究,通向公式找到就行; 步骤: 1.做特殊判断,空的话为true;调用recur函数,因为是判断左右子树是否对称,只有两个参数; 2.由上面的思路,点的左右子节点全部为null,或者是左右子节点的值相同,才是对称的,然后写出口条件,但是值相同要反着写,因为本层的值相同不能说true,还需要向下递归;但是本层不相同,那可以直接返回false了;所以写几个判断,递归结束就是两个有一个是null,或者值不同; 3.开始往下一层递归,就是上面的通向,要保证两个都成立,所以取一个与;
226.翻转二叉树
比较简单,找到出口为这个节点为null,本层的逻辑为,让左右子节点进行互换,然后递归到下一层,左右子节点都要进行递归,最后返回这个根节点就行; 步骤: 1.调用recur函数,这里不用做特殊判断了,因为recur函数中已经做了,当节点==null的时候,就返回节点; 2.recur的出口是什么,就是root==null; 3.本层逻辑是什么,就是让root的左右节点互换,然后开始下一层recur,但是调用下一层的时候,需要将左右节点都开始下一层;
106.从中序与后序遍历序列构造二叉树
写出来了,但不是很透彻,下次写的时候可能还是需要算,而且边界条件没有找好,不过还是值得肯定,自己思考思路然后自己代码实现,最后改错,最后测试成功,以后写高数之类的也可以这样总结,把一道题的思路搞的透彻可以解决一类问题; 按照以前的思路,考虑递归的出口就是当数组处理完了,这里面就是数组的左边界大于右边界;那每一层的处理逻辑呢,构建当前节点,然后递归找左子树,然后递归找右子树,那关键就是计算左子树的数组边界和右子树的数组边界;计算过程其实不难。
七:堆
总结:
1.介绍: 堆是能找到一组数据中的最大值或者是最小值,叫大顶堆或者是小顶堆。面试中主要是二叉堆,靠二叉树实现的,实际应用不是的,但是面试掌握二叉堆就行。 2.应用: 算一下top k问题,静态的话,用快排快一点,动态用堆; 算99%的响应时间的时候; 3.java中使用: 优先级对列:priorityQueue;
面试题 17.14. 最小K个数
分析: 可以使用大顶堆,堆顶是堆中的最大元素, 如果新来的数据比堆顶元素还小,那么说明堆顶肯定不是属于最小的那几个,就把堆顶元素弹出,然后把新元素放进去,有点类似于单调递减队列了,只不过堆的代码实现要简单一点;这里面需要注意一下,堆默认是小顶堆,如果想用大顶堆,需要加一个比较器进去; 步骤: 1.做一些特殊判断; 2.定义一个大顶堆,注意加比较器; 3.先把前k个元素,不管大小,放进堆里; 4.新的元素对比堆顶元素,如果比堆顶元素小,就弹出来堆顶元素,然后放进去; 5.最后把堆里的k个元素,放进结果数组中;(堆底层是存的数组,结果不在乎顺序的话,可以遍历数组)
239.滑动窗口最大值
堆虽然使用简单,但是插入和删除操作,时间复杂度有点高,最后没有能过了测试用例;
八:排序
1.快速排序
思路在b站公台所言极是中讲的很好,贴一下代码 public void quickSort(int[] nums, int left, int right) { if (left<right){ //分区,分区应该返回的是拿出来那个基准值的坐标 int index=partition(nums,left,right); quickSort(nums,left,index-1); quickSort(nums,index+1,right); } } private int partition(int[] nums, int left, int right) { //基本思路是默认取第一个当做基准值,然后进行while循环,只要两指针还不相遇,就一直循环 //先让不是空闲位置的那个指针一直循环,遇到需要换位置的就换,有了空闲位置以后,让另一个指针进行循环,一直到另一个 //位置有空闲指针; int pivot=nums[left]; while(left!=right){ while(right>left){ if (nums[right]<pivot){ //小的话,把这个值放在左边的空闲点,然后让这里成为了新的空闲点; nums[left]=nums[right]; break; } right--; } while(left<right){ if (nums[left]>pivot){ nums[right]=nums[left]; break; } left++; } } nums[left]=pivot; return left; }
快排过不了测试用例,修改方案是随机选点以后,把随机点与left值进行调换,这样可以不用改之前的代码; 为什么选头节点不行,随机节点就可以呢,因为选随机节点的话,可以更均匀一些,比如如果nums数组本身是有序的,那每次选头节点作为基准点都是最小的,所有的数都会到基准点右边,在递归的时候就不会走第一个递归了,那时间复杂度就会退化成n的平方。 //2.0版本:随机选点 int pivotIdx = new Random().nextInt(right - left +1) + left;//0-参数,随机数不包括参数本身 swap(nums,pivotIdx,left); private void swap(int[] array,int i,int j) { if (i == j) { return; } array[i] ^= array[j]; array[j] ^= array[i]; array[i] ^= array[j]; }
整体思路: 先找到一个基准值,然后将整个数组按照基准值进行移动,比基准值小的放在左边,比基准值大的放在右边,这样就算是排序是不是就是只在自己的半个部分排序,不用像其他排序方法一样,还得从头比较到尾走一圈。那左边的又可以进行重复,也找自己的基准值,然后划分左右,右边一样,就这样进行递归; 递归的出口呢,就是当传进来的数组的范围就剩一个的时候,不需要找基准点了,根本就不需要进行排序了,就一个了,没必要排序了,就直接返回就行了。 每一层递归的逻辑呢,就是找到基准点,然后把基准点左边的继续递归,基准点右边的继续递归; 那关键就是找到基准点,然后将数据划分到左右两边; 怎么找到基准点呢:找基准点不是个问题,刚开始的时候找的第一个值当作基准点,比较简单,但是过不了测试用例。后面改成了随机找一个基准点,然后将它与第一个数进行调换;调换的时候用的与运算,可以快速交换两个数的值,找随机数的api也要注意一下,因为平时不常用; 怎么讲数组根据基准点进行划分呢:基准点换到了左边第一个位置,现在也保存起来了,那数组的左边第一个位置就空闲了,可以放值了。所以可以从数组的右边开始while循环,只要 right>left,就可以一直while循环,判断right指针的值,如果大于pivot就不管,right指针左移,等于也不管,小于的时候说明要放在pivot左边了,而且左边也故意留的有空闲位置。这时候这个值放到左边去了,那右边这个位置就空闲了,就开始让左指针移动(right循环break),while循环左指针,大while循环停止,此时左右指针相遇了; 相遇的时候,这个位置是空闲的,用来存放那个pivot节点,所以代码里也记得这一行,最后将pivot的左边返回就行,就是left; 步骤: 1.写递归的出口,如果left==right就return; 2.每一层的任务是,找到pivot的坐标,将左边和右边也进行递归; 3.具体找pivot做的事: 先调用random找到这个值然后将它与左边头的值进行调换,用那个与运算,也不难; 找到pivot的值以后,将数组进行划分,先有一个大的while循环,条件为left!=right,然后移动右指针,因为左边有空闲,移动的时候也用while,条件也是left!=right,那什么时候停止呢,就是有指针值小于pivot,这时候右边while循环break,左边也是同理; 最后循环完毕以后,左右指针相遇,相遇的地方就是pivot的位置,pivot赋值,返回这个点的坐标就行;
九:动态规划
总结:
一:五个主要思考步骤 1.dp[i]代表的是什么; 2.递推函数,怎么从低到高或者是从高到低,这关系着后面的遍历顺序; 3.初始值,根据题目获取dp的初始值; 4.确定循环的顺序; 5.自己先人肉的走几轮测试一下,看写出来的代码是不是跟自己思路一致;
一维的dp数组:
509.斐波那契数
思路: 按照动态规划的5个步骤,找到递推函数,这个由于是已经知道斐波那契数列了,递推函数是现成的,初始值也有了,那dp[i]代表的就是第i个数的值,也符合题意。至于循环的顺序,因为这个数列是从小到大推的,所以顺序应该是从小到大; 步骤: 1.进行特殊判断,当n小于等于1的时候,直接返回n; 2.写dp数组,写初始值dp[1]=1,dp[2]=1; 3.进行for循环,从小到大循环,递推条件为dp[i]=dp[i-1]+dp[i-2]; 4.返回dp[n]; 补充:由于过程中只有两个数,所以可以不需要定义数组占用空间,用两个变量也可以表示;
746.使⽤最⼩花费爬楼梯
思路: 可能这道题比较简单,确实在简单的范畴里面,只要根据题意确定dp[i]是到第i层的最小花费,递推公式根据到第i层可以从i-2一下子走两层,或者i-1一下子走一层去决定,具体选哪个就看花费是哪个少一点。(至于会不会产生贪心算法的问题,这道题先不要想那么复杂,这纯属就是为了练习都动态规划用的。)这样递归公式也找到了,而且题目中也说明了dp[0]和dp[1]的值,所以初始值也有了。 关键还有是dp的数组长度要比cost大1,因为这是要登顶,不完全是跟斐波那契那个一样说是到第几层台阶。cost的最后一个值其实也是要加入计算的,最后一个dp[i]=dp[i-1]+dp[i-2]中的i-1其实就是cost的最后一个值,那i应该比最后一个值大1. 步骤: 1.进行特殊判断,如果cost长度小于等于1,则登顶就是台阶1,返回0就行。 2.定义dp数组,长度为cost.length+1,原因上面已经说了。dp[i]含义就是到第i层的花费,所以比cost应该高一层。 3.写初始值,题中很明确,dp[0],dp[1]=0; 4.for循环从2开始,因为已经有01了,i最大值为dp数组最大值,; 5.递推公式为dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
二维的dp数组:
注:二维数组表示 在 Java 的二维数组中:new int[3][4] 表示二维数组有3行,每行有4列 行是第一个维度,表示二维数组有多少个一维数组。 列是第二个维度,表示每个一维数组有多少个元素。 通过行索引和列索引可以访问二维数组的具体元素。 在动态规划中,对二维数组的初始化,一般都是初始化第一行和第一列。
62.不同路径
思考: 到了二维数组部分了。 先定义一个二维数组dp[i][j],代表的意义是第ij位置的可能路径的值,根据题意,机器人只能往右和往下走,则对任意的ij位置只能从上(i,j-1)或者是从左边(i-1,j)移动到这个位置,所以ij位置的可能性就是左边和上边可能性的和,那么递推公式就可以出来了。dp[ij]=dp[i,j-1]+dp[i-1,j]; 含义有了,递推公式有了,按照步骤开始算初始值,可以知道想象一个二维表格,第一行肯定只能从左往右移动这一种方式,所以第一行的所有的值都为1,同理第一列只能从上向下移动,第一列的所有值也都是1,那初始值就算找到了。 下面是循环的顺序,此题目根据图形只能从左往右从上到下循环,因为移动方向是这样的,只能通过左边和上边得到当前的值,不能从下边推导出来当前值。 步骤: 1.定义一个二维数组,第一个值为几行,第二个值为每一行有多少个元素(就是几列),dp[i][j]表示的含义是第i列第j行这个位置的可能路径值是多少; 2.初始化,遍历i从0到m得到第一列的初始值1,遍历j从0到n得到第一行的值1; 3.进行遍历,i j都是从1开始到末尾,递推公式是思路中写的dp[ij]=dp[i,j-1]+dp[i-1,j]; 4.返回最后一个位置的值;
63.不同路径2
思考: 与上一题类似,只不过可能要多一些判断,比如初始化赋值的时候,如果遇到了障碍物,则后面都是0了,(基础太薄弱了,思考的时候以为是把后面的都赋值成0,但是数组的默认值不就是0吗,还用赋值吗。)在后面递推的过程中,如果遇到了障碍物了,比如是左边遇到了,则只能从上边下来了,如果是上边遇到了,则只能从左边过来了。根据之前的递推公式,dp[ij]=dp[i,j-1]+dp[i-1,j]可以知道,如果遇到了一个障碍物,则那个的可能性就是0,让dp[i][j]等于另一个值的就行,比如dp[ij]=dp[i,j-1]+0,反之道理一样。那什么时候把它的值赋值成0呢,可以在到ij的时候判断一下上一个是不是障碍物,然后将它赋值成0,不过那可能要判断两次,倒不如在ij的时候判断一下当前这个位置是不是障碍物,然后将它直接赋值为0,通过上面的经验可以知道0不用赋值,默认值就是0,那就直接跳过这次for循环就行了。 步骤: 1.做特殊判断,如果起始点或者终止点是障碍物,就直接返回0; 2.定义数组,obstacleGrid.size就是二维数组的维度,obstacleGrid[0].size就是里面每一个数组的维度,根据这个得到新的数组dp的m,n的值;含义就是dp[i][j]表示当前位置的可能路径值; 3.初始化,根据上面思路,还是定义两个循环,遇到障碍物则后面的不需要赋值了,默认0; 4.for循环,循环条件都是正常的,但是需要判断是不是障碍物,如果是则赋值为0,开始下一次循环; 5.最后返回最后一个位置的值即可;
343.整数拆分
总结: 这道题也没学到什么东西,思路可能比较难想,但是套用动态规划步骤的话,可能能得到答案,定义dp数组的含义,很明显就是dp[i]表示数值i拆分后乘积的最大值。初始化:dp1=0;dp2=1。递推公式需要思考一下,比如10,就需要对10进行遍历,从1到9拆分看哪个小,但是不能简单的 将递推公式写成 max(dp[i],j*dp[i-j]),因为这样的话,dp[i-j]总是会把粒度拆的很小,有时候可能两个数的乘积比三个数更大,比如对6来说,3乘以3得到9,2*2*2是8,所以两个数乘积可能更大,并不是数值拆分的越细越好。这时候递推公式就需要加上 j*(i-j)这个乘积大小比较了。动态规划步骤一致,主要是递推公式和循环条件做好。
动规:背包问题
1.01背包理论基础
具体题目可以看讲义中的01背包问题,这个题目在定义dp数组含义的时候就很麻烦,不过还好,dp[i][j]表示在i个物品里,背包容量是j的时候的最大价值。然后比较麻烦的是递推公式,找出来递推公式基本是解题关键了,很难想,具体思考的时候基本就是从小的推导大的,本题中i从小一步到大一步的时候,就是对应着dp[i][j]是怎么来的, 如果以物品为中心的话,可以表示为i-1是不放这个物品的时候,i是放这个物品的时候,从i-1到i就是放了这个物品。如果j能容纳这个物品的话就是dp[i][j]=dp[i-1][j-w[i]]+v[i],它的最大价值就是把i重量减去时候的最大价值,加上i的价值;如果j不能容纳这个物品,则dp[i][j]=dp[i-1][j],就是如果背包容量不变,我装下i-1物品范围的最大价值就是i物品范围的最大价值,反正多一个不多,我也装不下,就算是dp[i+5][j] 这个推导思路不对。 对于dp[i][j]是怎么来的,对于第i个物品可以选,也可以不选,什么情况下不选呢,就是包根本放不下,j压根就小于i的重量,或者说是选了以后还没不选的价值大,就是还不如不选,那你说如果选了以后没有不选的价值大,那dp[i][j]的最大价值是啥,肯定是dp[i-1][j],哈哈哈,得出来了不选的情况了。那如果是选了以后价值大呢,那这时候肯定是要选i的,至于剩下的空间,就还让他尽可能的价值大,剩下空间是多少呢,[j-w[i]],这是剩下的空间,那可选择的物品呢,是[i-1],所以怎么让他尽可能大呢,那就是子问题了,就是dp[i-1][j-w[i]],那这时候dp[i][j]就=dp[i-1][j-weight[i]]+value[i];那到这里递归公式就出来了。
背包状态数组的压缩
1.等于把i给去了,只要j的那个一行数组,一层层的重复使用;dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 提问:压缩后的一维dp数组的方法进行遍历的时候,为什么j背包容量这一层,需要从后往前遍历。 回答:你看递推公式 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 后面dp[j]在获得值的时候,需要使用到前面小一点的dp[j-w[i]]的值,现在只有一个数组在复用,所以这个值肯定不能覆盖啊,上一层的前面的值得存在啊,所以就得从后面开始遍历,哈哈。而且从上面的二维分析可以知道,dp[i][j]的值只与上一行左上角的dp值有关,所以如果保留一维的话,需要从右往左遍历。 提问:递推公式是怎么从二维得到的? 二维是:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 回答:从上面二维分析可以知道,本层的[ij]位置的值只与上一层有关,所以需要两行数组就行了,但是如果复用上一行的话,那一行就够了。所以i位置其实是用的i-1的位置保存值,这也是为什么需要从后往前遍历了。
416.分割等和⼦集
思路: 题目上来很难将这个问题和01背包联系到,因为01背包是在说容量为j的背包能装的最大价值,如果按照题目,可以将价值理解为跟重量一致,那就是容量为j的背包所能装的最大的容量,最大肯定是装满,也就是说要判断容量为j的背包的最大价值是不是j。 既然我们动规的背包问题求出来的结果是容量为j的背包在[1-i]里所能选的最大价值,那既然能确定选出来的是最大的话,那就可以用这种方法。 那这个j是多少呢,肯定不能是nums数组的和啊,因为就算最后算出来容量为sum的背包装了所有的数,价值是sum,但是没有意义啊,不能证明这些数就是可以刚好分成2组的。 那应该是多少,sum/2,这样如果选出来一个组合的价值是sum/2,那说明剩下的一个组合肯定是sum/2,这是肯定的,所以背包的容量j也就确定了。 动规步骤: 0.首先应该是建模思维,问题联系到用动规的哪种模型去解决,这一步是比较麻烦的,上面思路中已经分析了,确定了用动规以后,开始进行动规具体步骤处理。 1.确定dp数组含义,dp[i][j]容量为j的背包在【1-i】中能选择的最大价值,这里面的含义也是一样的。 2.确定状态转移方程,dp[i][j]=Math.max(dp[i-1][j],dp[i-1]dp[j-weight[i]]+value[i]),这里的价值与重量是相同的,所以是value[i]就是weight[i]; 3.初始化:dp[0][j]=0,没得选肯定都是0,怎么不初始化dp[i][0]呢,因为后面想用状态压缩方程,不需要它了。 4.遍历:由于是想用压缩方程,所以遍历从i作第一层,j做第二层,j从大到小,大是sum/2,小是j<=nums[i],因为如果我连一个都放不下,那也没必要进行计算了,肯定是0或者等于以前的值,放1到(i-1)的进去,那就不需要遍历这一层i了,反正都用的以前的。
1049.最后⼀块⽯头的重量II
思路: 这种题目的难点都是难在建模上,怎么思考问题与背包模型联系到一起是关键所在。题目中两两相撞,大的会把小的抵消了,一直抵消,到最后只剩下一个。相当于是如果有两组数据,这两组数据的和相同的话,那最后肯定全部抵消了,就是返回0,否则就是说明不相等,那肯定是有一组数据的值小于总和的一半的,小于sum/2,如果sum是奇数,那可以肯定两组数据肯定不相同,肯定有一组数据的最大重量就算最种也是sum/2,所以将sum/2当做是背包的最大容量是可以的。就看sum/2的背包能装的最大价值,可以装不满,只要算出来最大就行,装的这些都可以被碰没有,sum减去2倍dp最大价值剩下的就是最后碰下来留下的大小。 动规步骤: 0.建模,上面分析过了。 1.确定dp数组含义,dp[i][j]表示容量为j的背包能装的【0-i】的最大价值,; 2.状态转移方程dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]),价值重量相等,都是nums[i]; 3.初始化dp[0]=0,背包容量是0,没得选就是0; 4.遍历:i是从0开始,如果是[1-i]里面选的话,可以从1开始,因为dp[0][j]一般为0,没法选肯定是0。但是现在想直接用nums数组,数组是从0开始的,所以i需要从0开始遍历,j还是从大到小,大是sum/2,小是nums[i];
494.目标和
思路: 这个题还是建模比较难,这几个题的共同点就是想办法可以把数据分为2等份,然后就找到了背包的最大容量,然后求最大价值就可以了,看看这个最大价值是不是满足题意的。 这里的dp数组含义稍微有点不同,dp[i][j]表示的是从1到i中选和为j有多少种方案,然后确定状态转移方程,[i][j]位置是由什么转移过来的,根据含义可以知道,i位置的数据可以选,也可以不选,如果不选的话,dp[i][j]=dp[i-1][j],就是和为j有这么多种方案, 如果选的话,dp[i][j]=dp[i-1][j-nums[i]],意思是i这个已经确定选了,所以剩下的还需要在1到i-1里选出来和为(j-nums[i]),即等于dp[i-1][j-nums[i]]的方案数。所以这时候就可以知道选与不选是两种方案,每种方案都对应着几种可能性,所以现在的可能性个数就是把两种方案都加起来。得到 dp[i][j]=dp[i-1][j-nums[i]]+dp[i-1][j]; 初始化呢,因为一会要压缩数组,所以初始化可以只考虑j了,即dp[0]的值,代表着(1到i)中选择和为0的可能性,那只有一种可能,就是不选,空集就行。则dp[0]=1; 动规步骤: 0.建模,上面分析了; 1.确定dp数组含义,dp[i][j]表示1到i中凑和为j一共有多少种可能; 2.转移方程:dp[i][j]=dp[i-1][j-nums[i]]+dp[i-1][j]; 3.初始化:dp[0]=1; 4.遍历,如果是二维数组则正常遍历,如果压缩的话,还按照i从小到大,j从大到小。
2.完全背包
相关描述
完全背包与01背包的区别是,每种物品可以被选择多次,也就是状态转移方程的写法:本来选i的时候是 dp[i-1][j-nums[i]]+values[i], 现在选i是 dp[i][j-nums[i]]+values[i],因为i可以被重复选很多次,只要他愿意。状态方程的区别就在这里。
二维的代码是
public static int completeKnapsack(int N, int W, int[] weights, int[] values) { // 定义一个二维数组 dp,dp[i][j] 表示前 i 种物品中,容量为 j 时的最大价值 int[][] dp = new int[N + 1][W + 1]; // 初始化 dp 数组 for (int i = 0; i <= N; i++) { for (int j = 0; j <= W; j++) { dp[i][j] = 0; } } // 填充 dp 数组 for (int i = 1; i <= N; i++) { for (int j = 0; j <= W; j++) { dp[i][j] = dp[i - 1][j]; // 当前容量放不下该物品, if (j >= weights[i - 1]) { // 当前容量可以放下该物品, // 完全背包问题可以选择多次,因此考虑多个该物品 dp[i][j] = Math.max(dp[i][j], dp[i][j - weights[i - 1]] + values[i - 1]); } } } // 返回最大值,即 dp[N][W],表示前 N 种物品在容量为 W 时的最大价值 return dp[N][W]; }
从代码的状态转移方程可以看出,想象成那个方格,dp[i][j]是从本层的左边得到的,所以转成二维数组的时候,遍历顺序是从左到右的,也就是从容量小的到容量大的。这样一维数组的for循环的顺序就确定下来了。