剑指offer题解(56):

03:数组:第一种做法:无脑遍历,时间复杂度:O(n^2)。第二种做法:先将数组排序,然后进行遍历,只要相邻两个相等就行,O(nlgn)。第三种做法:使用额外的数组,将当前位的值作为数组的下标,判断其是否出现过就行 O(n)

04:数组:因为数组的排列是满足一定的规律的,也就是从左往右逐渐增大,从上往下逐渐增大,那么以右上角的元素作为一个基准,如果目标元素比当前要大,那么则往下面一位继续搜索,如果目标元素比当前要小,那么则往左边搜索。

05:简单:遍历替换即可。

06:链表:第一种做法:很简单就能想出来,先遍历一遍得到链表的长度,在新建结果数组将其倒序赋值即可。第二种做法:使用栈,先将链表从头到尾依次插入栈里,然后逐个pop给结果数组,一样的思想,只不过用到了栈。

07:树(分治递归):前序遍历的第一个节点必然是根节点,而中序遍历中该根节点的左边的值都是左子树,右边的都是右子树,所以利用这一特性,先在前序中找到根节点,然后去中序找找到该根节点的下标,进而可以区分左子树和右子树,然后在以左子树的根节点和右子树的根节点进行遍历,不断的分治递归:

node.left=findnode(preorder,root+1,left,i-1);

node.right=findnode(preorder,root+i-left+1,i+1,right);

09:栈:一个栈a用来添加元素,一个栈b用来删除元素;首先判断栈b如果为空,那么进而判断a如果为空,则返回-1;如果a不为空,则将a里面的元素全部pop进b里面,很巧妙,因为元素只需要存储在一个栈里即可,这样返回b 的栈顶pop即可。

10-1:动态规划:很简单,但是我第一时间没有想出来。并且在评论区看到一个更加简易的做法:使用两个数来代替的dp数组,并且在每次循环中进行更新,也就是n=(n-1)+(n-2),之对后面两个进行循环更新即可。

10-2:动态规划:和上一题一模一样的思想。

11:二分查找:思想很重要:就是将数组从中间分开,每一步循环都会排除一半,选择新的一半重新进行中间值的遍历;首先对于找出最小值来说,一步一步的缩小,每次的循环先判断mid与right的关系,如果mid<right,那就说明right所在的这一位到mid这一位中间的数肯定不是最小值,这时更新right的值为mid;如果mid的值大于right的值,则更新left的值为mid+1;而本题还有一个特殊的情况就是会有重复的值,这样的话就让right-1,因为有与之相等的值还在判断范围里。

12:深度优先搜索:也是一个回溯递归搜索的思想,dfs函数为当前i,j以及要求字符串k位判断,如果i,j溢出数组长度,以及[i][j]!=[k]时,返回false,否则则继续调用dfs函数对其上下左右四位进行遍历,值得注意的是,如果当前位满足的话,在进行其周围的遍历时,要将其设为空,然后在进行遍历完赋予其原来的值,这也就是回溯的思想了;结束条件:k到达了目标字符串的大小。

13:递归搜索:从当前位开始,首先进行判断,如果满足条件,那么说明该位可以走,进而从该位来进行其右边和下边的遍历,同时对当前位进行标记,这样下次如果在走到的当前位,就不需要在遍历了,最后直到不能走,返回结果即可。

14-1:动态规划:dp[n]表示n时的最大值;对于当前位i而言,依次设立一个j进行遍历,有两种情况:j*(i-j)或者j*dp[i-j],只需要找到其较大的即可。

第二种做法:采用数学计算的方式,找到规律,拆分成3越多,值越大,所以讲n对三取余,进而进行余数的判断来求得最大值(余数为1的话,要拆分一个3,变成2+2)。

14-2:动态规划:采用上一题的第二种方法,但是不同的是要在每一步对result进行赋值的时候,进行取模操作。

15:位运算:java中的无符号二进制移位:>>>1。对于本题来说,第一种办法:每次只比较最右边一位,将其与1进行&操作,则可证明该位是否为1,然后将原操作数右移一位,进行下一位的判断。第二种方法:巧妙的使用n&(n-1),n-1则会使n变成最右边为1的一位变成0,而该位的右边的从0变为1,然后在与原来的n进行&操作,则可让n的最右边为1的一位变成0,而其他位不变,这样每次使得result+1即可,而循环的次数也就是最终结果,这样比刚刚那个一位一位的判断节省了内存。

17:数组:如果不考虑大数的情况,很简单;

18:链表:很简单的删除链表节点:将当前位指向下一位的下一位即可完成删除。

19:动态规划:很难:首先设立dp数组为前i位的s字符串与前j位的p字符串是否匹配,然后进行一个初始化,对第一行的偶数列进行*号判断以及匹配:dp[0][j] = dp[0][j - 2] 且 p[j - 1] = '*':首行s为空字符串,因此当p的偶数位为*时才能够匹配(即让 p 的奇数位出现0次,保持p是空字符串)。然后就是主函数的循环:当p的当前位不是*时,判断s和p当前位相等或者p当前位为 . 那么说明dp[i][j]=dp[i-1][j-1];当p的当前位是*时,有两种情况:一种是忽略p的当前位和上一位,看看是否匹配,也就是:dp[i][j]=dp[i][j-2];第二种情况为s的当前位与p的*前一位相等或者p*的前一位是 . 那么这时:dp[i][j]=dp[i-1][j]。

21:数组:第一种方法:很简单的遍历,如果当前位是偶数,那么从数组后面开始寻找第一个奇数,与其交换即可。第二种方法(一次快排):快慢指针:一个指针left从前走,一个指针right从后开始走,left找到第一个偶数,right找到第一个奇数,然后二者交换,直到left=right为之结束。

22:链表:快慢指针,很简单,当快指针走到头时,慢指针所在位置即为所求。

24:链表:原地翻转,使用两个指针,依次按位翻转即可。

25:链表:正常遍历即可,依次比较l1和l2的val大小,取较小值插入到结果链表中,然后最后将还没遍历完的一条直接接到最后即可。

26:树:使用一个辅助递归函数:issame(Node A,Node B),用来判断A和B是否符合子树,满足条件:(A.val==B.val)&&(issame(A.left,B.left))&&(issame(A.right,B.right));而在初始的函数里面。进行每一位的遍历,使得A的每一位都可以作为根节点和B进行比较,只要满足一个即可,判断条件:issame(A,B)||isSubStructure(A.left,B)||isSubStructure(A.right,B);

27:树:很容易想到构建一个辅助递归函数,一位一位的镜像翻转即可,创建一个新的树head也就是翻转之后的树,对原来的树root进行递归遍历,只要当前节点的子节点不为空,则将其赋值给head的相反的子节点即可(这个方法需要重新申请内存,也就是和原来的树一样的内存调用)。第二种方法:在评论区看的,将root原地翻转,不需要额外的内存;递归函数就是原来的函数自己,也就是让root.left=mirrortree(root.right),值得注意的是,需要先提前保存一下root的left节点,然后再将其放入函数赋给right。第三种方法:使用辅助递归函数,来进行上一种方法的操作,也就是将其分开了,其实还不如第二种方法来的好看。

28:树:搞清楚递归辅助函数的作用,以及结束条件,对于本题而言:传进递归函数的为根节点的两个子节点,当两个节点的val值相等并且:其子节点,也就是左左等于右右、左右等于右左时,在可以返回true。

29:数组:需要自己划分出界限,以及每一步每一步的标识:left,right,top,last,分别代表当前的上下左右的界限,每一次循环都对四个边界进行添加,并且分别判断四次,只要有一次超出数组边界,那么即可break;举第一次循环的例子:当前for循环里i=left,因为当前是第top行的判断,所以数组的下标为[top][i],最后将上边界下移一位:top--,并且判断top与last的大小,是否越界;同理,下左右的判断也是如此,思路一定要清楚(在纸上画出来很明显的可以写出)。

30:栈:因为要求了时间复杂度为(1)所以要使用一个辅助栈来保存当前的最小值,在每次进行push操作时,对辅助栈的栈顶元素进行比较,如果当前操作值更小,则将其push到辅助栈中,同样在pop的时候,操作值与辅助栈的栈顶进行比较,如果相等则说明辅助栈也需要pop。注意如果在==上报错,可修改为.equals()。

32-1:树:从上到下层次打印树的节点,使用队列来实现,因为很适合,先将根节点放入队列中,然后进行遍历,只要队列不为空,先取得队列头部的节点,然后输出其值,然后将其弹出队列,同时将其两个子节点压入队列中。

32-2:树:广度优先遍历,和上一题一样,只是多了一个要求,只需要在每次遍历的时候,先获取到队列的大小,这也就是这一层的节点数量,然后进行相应数目的遍历出队列以及子节点的入队列,但是这时子节点的遍历这就由下一次循环赋予给下一层的数组。

32-3:树:也是在上一题多加了条件,同样做出修改,在每一次循环中遍历两层,也就是先正着遍历一层,并且将其子节点按照左子树先右子树后的顺序插入队列尾端;然后进行第二层的遍历时,从队尾开始,并且将其子节点按照右子树先左子树后的顺序插入队列首端,这样下一次遍历又是正着遍历,直到队列为空结束,值得注意的是,因为每次进行两层的遍历,所以第二层或许为空的,所以添加一个判断条件来决定是否加入结果数组。

33:树(递归):搜索二叉树的后续遍历时有规律的,根节点在最后,然后从前往后先是一直比根节点小,然后一直比根节点大,利用好这一特性开始递归。首先找到从前往后第一个比根节点大的,从此下标到根节点一定是比根节点都要大的,如果不满足,则返回false;其次判断当前节点的左子树和右子树是否满足,递归调用本身即可。

34:树(回溯):回溯的思想,一定要搞清楚回溯函数的意义,数的结构已经给出了递归树,所以比较明显的可以看出来,每次调用back函数,都对其nowsum进行更新以及与sum的比较,满足条件则加入result中,然后将该节点的子节点进行下一步的遍历,并且在子节点遍历完需要回溯,也就是将temp里面添加进的这一位的节点remove掉,才不会影响下一次遍历,值得注意的是在将满足条件的temp加入result时,需要深拷贝一个新的temp数组才可以,不然加入的temp数组都一样。

35:链表:深拷贝问题,不同于普通的复制,本题是重新创建新的对象,新链表的每一位都是新的内存;首先复制链表的节点,然后复制链表的random,最后进行裁剪,将原链表复原,以及裁剪出新的链表。

36:树(二叉搜索树转化为双向链表):使用二叉搜索树中序遍历为递增序列这一特点,在每次遍历的时候改变为将其变为双向链表;使用一个全局节点before用来表示当前节点的前一个节点,开始递归:首先调用本身开始left左子树的递归,进而才做出改变,最后调用自身函数对右子树进行递归;主要思想:首先判断before如果为空的话,说明当前节点为头结点,将其设为head,如果不为空,那么将before的right指针指向当前节点root,然后将当前节点的left指针指向before,进而更新before为当前节点,然后进行下一次遍历;在所有的节点遍历完之后,before为最后一个节点,head为首节点,这时需要将其首尾相连,也就是before.right=head; head.left=before,最后返回head节点即可。

38:回溯:很难;难的点在于想出要怎么回溯,也就是数组要怎么有规律的变化才可以得出最后的结果:两两交换,然后以交换后的数组再来进行后面的交换,同时进行完之后回溯,再以没有交换前的数组进行当前位后面的交换;同时要进行剪枝,如果当前位在之前出现过,那么直接return,不进行当前后续操作即可。

39:数组(位运算);第一种方法:现将数组排序,然后输出中位数即可;第二种方法:摩尔投票法,因为该题的众数是永远多于数组大小的一半的,所以对数组里面的数进行一个sum的统计,如果当前sum=0,那么就默认当前位为众数,sum++;如果sum不为0的情况,当当前位等于默认的众数时,sum++;如果不等于那么sum--,直到sum==0进行众数的更替,这样一个一个的抵消,因为众数数量是多于数组大小的一半的,所以总能找到他。第三种方法:位运算,还是因为众数的数量是多于一半的,所以二进制的每一位取1还是取0,如果在数组中进行遍历,那么其也是多于一半的,所以进行一个32次的循环,每次循环确定一位,将数组中的数的该位进行统计,如果1出现的次数大于一半:nums[j]>>i&1==1,那么该位为1,也就是result^=(1<<i),这样进行32次判断,即可确定最终结果。

40:快排:第一种方法:很简单就能想到的,维护一个k大小的数组,并且在k之后的加入时,判断当前结果数组的最大值,将其与当前要加入的元素值进行比较,留下较小的那个即可。第二种方法:直接排序,然后输出前k个元素;第三种方法:快排;普通的快排就是将数组全部排序,而在本题中只需要得到前k个即可,所以每次开始循环的时候,对当前位于k进行比较,可以节省一些遍历次数。算法思想:使用两个辅助函数,一个函数Partition为确定当前位,第二个函数DoPartition调用第一个函数以及进行自身的遍历;Partition:设立当前位的left下标值的元素为本次函数要判断的基准点,先从右边界往左遍历,直到找到第一个小于基准点的元素,将left设为该点的值,然后从左往右遍历,找到第一个大于基准点的元素,然后将其赋值给刚刚交换的right所对应的的下标,然后再次进行上述循环,直到left超出right,然后这时跳出循环,将刚刚基准点的值赋给left,也就是说在本次函数结束的时候,会返回基准点元素所在的下标,该下标左边的元素都小于它,右边的元素都大于他;而第二个函数:DoPartition:首先调用Partition,以0和数组长度为参数传入,然后检测Partition函数的返回值与k作对比,如果相等那么直接return即可,而数组的前k个即为所求;如果比k大,那么说明,结果在左边界到当前返回值下标里面,将右边界设为当前返回值下标-1;如果比k小,那么则置换左边界为当前返回值下标+1;继续调用本函数进行遍历。最后在原函数中调用一次DoPartition即可,然后返回数组的前k位就行。(如果是对数组全部排序,则直接在DoPartition中依次调用即可)

42:动态规划:其实严格来说这个也不算动态规划,这道题最主要的思想就是每次循环都更新max的值,这样的话对dp其实也不是太大的要求,当前位的dp值由前一位的dp值+当前位的nums值和当前位的nums值哪个大决定,进而对max来进行比对,取较大值为max的值。整个下来也就是判断当前位的正负是否要加入,以及前面算得的和的正负决定是否要加入,然后与max进行比对,更新他的值。

如果不使用dp数组的话,也挺好理解;使用一个sum值以及结果max值,对于每一位而言,判断当前的sum值:if sum>0,sum+=nums[i]; else sum=nums[i];这样是因为如果sum值为负的的话,直接使用当前的nums值会比较好,同时在这之后,将新得到的sum值与max值进行对比,取较大值,这样就算sum清空变为当前位变更小时候,max的值也不会变。

45:快排:使用快排的思想,只不过改变了比较的条件,然后思路和快排是一样的。

47:动态规划:很简单的类型,一眼便知,dp数组为当前位所能到的最大值,也就是其左边一位或者上边一位的dp较大的加上本身的值即为当前的最大值。

48:快慢指针:快指针为当前每一位的遍历,慢指针到快指针之间没有重复元素;快指针循环遍历数组,对于数组的每一位,将其与慢指针和快指针之间的元素进行比对,如果有相等的,则将慢指针换为相等的后一位,将当前位加入到结果中与max进行比对,取较大值即可。第二种方法:哈希表:对于每一个元素遍历时,都要在慢指针到当前位再来一次循环判断有无当前位元素重复,所以可以直接使用哈希表来代替这一循环,key为当前位的元素,value为数组相应下标;在每次循环的时候,在哈希表中查询是否有当前位的key:containsKey,然后将其value值也就是其相应的数组下标与当前的slow慢指针进行比对,取较大值,因为哈希表中有些存储的可能是之前被舍弃的,并不在慢指针的范围里,然后以这个key值将当前的下标更新value,并且更新max的值即可。

49:动态规划:当前第n位,则是由之前满足条件的*2或者 *3或者*5得到的,所以利用这一特性,一步一步的找,每次更新当前3个数乘出来的最小值为当前dp数组的值,然后将相应的是*2还是*3还是*5得到的+1,直到统计了n个即可。

50:哈希表:首先遍历一遍字符串,统计每个字符出现的次数,key表示字符,value表示次数,然后在进行一次遍历,遇到value=1的时候,即返回当前的key值即可。

52:链表:方法一:使用两个栈,将两个链表放入,然后顺序弹出进行是否相等的比较,若是碰到不相等的则break即可; 方法二:使用两个指针 node1,node2 分别指向两个链表 headA,headB 的头结点,然后同时分别逐结点遍历,当 node1 到达链表 headA 的末尾时,重新定位到链表 headB 的头结点;当 node2 到达链表 headB 的末尾时,重新定位到链表 headA 的头结点。这样,当它们相遇时,所指向的结点就是第一个公共结点。

53-1:二分:首先二分找到右边界的下标,然后二分找到左边界的下标。设立左遍历变量i,以及右遍历变量j,当当前mid中间位置的元素小于等于target时,那么i=mid+1,否则则是更新j=mid-1,因为右边界是第一个不为target的下标值,所以在判断的时候要加上=target,最后i即为右边界,因为这时的j是刚好在i后一位的,所以可以直接判断一下nums[j]如果不等于target,那么可说明其数目为0;同理二分查找左边界,在判断时不需要加上=target,最后的j即为左边界;最终结果为:right-left-1;

53-2:二分:因为下标和元素大小正常情况下是相等的,所以使用二分的判别条件就是下标是否等于元素值,因为不存在下标大于元素的情况,只有可能大于或者等于,而若是等于则说明从当前mid位的右面开始搜索:i=mid+1;若是不等于,则说明要从当前mid位的左边开始搜索:j=mid-1;当i超出j时,即左边界超过了右边界,则停止遍历,当前的i位即为所求,其实结果为nums[i]-1,也就是i的值;为什么是i的值而不是j,因为当找到的时候,还要在进行一次遍历才能跳出循环,这时当前位mid=i=j满足nums[mid]>mid,所以j会--,这时跳出了循环,i也就大于了j。

54:树:因为二叉搜索树满足中序遍历之后的数组为从小到大排序,所以很明显可以先遍历二叉树,然后返回相应位的值,使用递归遍历即可;所以这样的话很容易想出第二种方法,也就是只遍历要求的位数即可停止,这样节省了空间和时间。

55-1:树(回溯):简单,跟之前做过的回溯一样的做法,而且本身树的结构也很适合看出来回溯。

55-2:树(回溯):同样也是从顶向下开始遍历判断当前节点的深度,并且如果左右相差小于1、以左右节点为根节点也同样满足,才是满足平衡二叉树;回溯函数也就是用来求当前节点的深度的,在主函数里调用进行比较。

57:数组(哈希):第一种方法:哈希:使用哈希表将数组中的元素为key值放入,然后进行遍历,如果哈希表中存在等于target减去当前元素的key,那么这两个值即为所求。第二种:双指针:两个指针一前一后,如果当前两个指针相加小于target那么left指针后移一位,如果大于则right指针后移一位,直到等于即可。

58-1:数组:使用一个辅助栈,从后往前遍历,先定位到第一个不为空格的字符,然后将其压入栈中,直到碰到下一个空格,然后设立一个stringbuilder将其依次弹出,最后append到结果string中,并且额外添加一个空格,循环上述操作即可(注意下标越界);最后跳出循环之后,再减去一个空格即可。

58-2:字符串:第一种就是先将String转为字符数组,然后一次向左移动一位,进而在最后补上一位,移动相应的位数即可。第二种:直接使用substring。

59-1:队列:使用队列来表示当前滑动窗口的最大值序列,所以队列是一个单调递减队列,队列存储的是nums的下标,这样方便判断是否超出了限制;从nums的0开始循环,而当i大于k-1的时候,才将队头元素放入结果数组中;首先判断队列存储的队头是否超出了滑动窗口的k值,然后进行循环只要是当前的nums[i]大于队尾元素,则将队尾pop,然后直到不再大于,将当前位的nums[i]放入队列,这样队头永远是当前最大的值,并且没有超出滑动窗口的限制,在队头pop了之后,因为队列是单调递减的,这时下一位也就变成了当前最大的值。

59-2:队列:因为要求了时间复杂度,所以需要使用一个辅助队列用来记录当前的最大值;在每次进行push操作时,对辅助队列进行循环判断,如果当前队列末尾小于要操作数的时候,对辅助队列进行pollLast操作,直到辅助队列的末尾值大于操作数停止(也就是说在之前添加进队列的值里面,当前位是最大的),这时将此值offer进辅助数组中。在进行pop操作时,判断辅助数组的队头元素是否是该值,若是则一起进行poll。

61:数组:这题很巧妙,其实只要找到数组中的最大值和最小值,两者相差不到5即可满足题意;先将数组排序,然后统计第一个不为0的下标,并且判断数组中是否有重复的数,最后判断nums[4]-nums[第一个不为0的下标] 小于4即可。

63:动态规划:刻在骨子里的动态规划,很简单,并且可以使用一个变量来代替dp数组,设立循环使result取自身和当前价格减去前面的最小价格的较小的值,然后在让当前的最小价格与当前位的价格对比,取较小值更新最小价格即可。

68-1:树(递归):很巧妙,从顶向下开始遍历,如果当前节点比目标两个节点都大,那么则以左节点开始遍历,若果当前节点比两个目标节点都小,那么则从右节点开始遍历,若是比一个大比一个小,那么当前节点即为所求节点。

68-2:树(递归):跟上一题类似,只不过递归遍历的时候,需要从根节点出发,在左子树和右子树一直找p和q,并且会出现三种情况:如果left没有找到,那么说明,right即为结果;同样当right为null时,left为最终结果;当二者都不为null时,当前节点为结果。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值