剑指Offer刷题笔记

33、【树】判断一个后序遍历序列是否是二叉搜索树(二叉搜索树:左<根<右)

解法:递归。根据后序遍历的性质,最后一个数字是根节点。首先需要找到左右字数的分界点。遍历数组,找到第一个大于root的位置即为左右子树分界点,从分界点开始继续遍历,如果出现小于root的值,表示右子树不满足二叉搜索树定义,直接返回false。否则继续递归判断左子树和右子树是否是二叉搜索树。

边界条件:空树

 

34、【树】二叉树中和为某一值的路径(不止一条)

解法:题目要求输出路径。因此考虑用一个vector来保存走过的节点,push_back方法用来存入值,pop_back方法用来删除值,这样就可以形成一个后进先出的结构。按照前序遍历的顺序,遍历到一个节点的时候,首先判断该节点是否是叶子结点,如果是叶子结点且sum == target,则找到一条路径,输出该路径。如果是叶子结点但sum != target,则说明该路径不是要找的目标路径。找到叶子节点,说明寻找完毕,从vector弹出叶子节点。如果不是叶子结点,则递归进行左右子树的遍历。

边界条件:空树

 

38.1、【字符串】字符串全排列(此题编码不容易)

例:a、b、c的所有字符串排列为abc、acb、bac、bca、cab、cba

解法:首先确定第一个位置的元素,然后一次确定每一个位置,每个位置确定时,把所有情况罗列完全即可,注意有重复字符时,跳过

剑指Offer刷题笔记_第1张图片

 

38.2、【字符串】字符串组合(此题编码不容易)

例:a、b、c的所有字符串组合为a、b、c、ab、ac、bc、abc

解法:可以把求n个字符组成长度为m的组合问题分解成两个子问题,分别求n-1个字符中长度m-1的组合,以及求n-1个字符中长度为m的组合

剑指Offer刷题笔记_第2张图片

 

39、【TOP-K】无序数组中出现次数超过一半的数字

解法一:类比快排原理,当一个数字出现次数超过一半时,排序数组中的中间位置一定是该数字。利用partition函数,任取一个枢轴(如区间最后一个数),将小于枢轴的值移到左边,大于枢轴的值移到右边,每次partition都可以将一个枢轴元素置于正确的位置。循环该过程,直到partition函数返回的枢轴位置等于 length/2 。时间复杂度O(n)。注意:这种方法只有在原数组可以修改的情况下使用

解法二:你们全上都打不过我。利用数组保存两个值,数字本身和数字出现的次数n。遍历数组,遇到相同的数字时,n++;遇到不同的数字时,n--,直到n ==0 时,更新数字。循环以上过程,最后留下的数字就是出现次数超过一半的数字。

边界条件:数组中只有一个数字

 

40、【TOP-K】寻找n个整数中最小的k个数

解法一:类比39题,利用partition函数,任取一个枢轴(如区间最后一个数),将小于枢轴的值移到左边,大于枢轴的值移到右边,每次partition都可以将一个枢轴元素置于正确的位置。循环该过程,直到枢轴位于第k个数所在的位置(k-1)。时间复杂度O(n)。注意:这种方法只有在原数组可以修改的情况下使用

解法二:最大堆。维护一个大小为k的最大堆。通过删除堆顶元素来保持堆的大小为k,当所有数据遍历一边之后,堆中的最大值即为第k大的元素。时间复杂度:插入删除元素O(logk),共有n个元素,因此总的时间复杂度为O(nlogk)。该方法适合处理海量数据

边界条件:k = 1,k等于数组的长度

 

41、【DP】数据流中的中位数(排序后,中间位数的数字,注意区分奇偶数)【了解即可】

解法:将数据分成两部分,左边部分的所有数字都小于右边部分。用最大堆处理左边数据,最小堆处理右边数据,注意保证两个原则:

1、左右数据数目之差不超过1

2、最大堆的所有数据都要小于最小堆的数据

当新数据插入且以上两个原则不能满足时,删除最小堆的堆顶数据,并将其插入最大堆中,同时将新数据插入最小堆

 

42、【DP】连续子数组的最大和,数组中包含正数和负数,要求时间复杂度为O(n)

解法:考虑动态规划,设dp[i]为以i结尾的连续子数组的最大和,则dp[i] = max { dp[i-1] + nums[i] , nums[i] },含义:在前面已经得到的子数组后加上nums[i]或者自成一派,自己最大。

(注意:题目中出现“连续子数组”,dp[i]都要考虑以第i为结尾,这样才能保证连续)

边界条件:数组为空

 

43、1~n整数中1出现的次数

 

44、数字序列中某一位的数字

 

45、【字符串/DP】把数组排成最小的数

例:数组{3,32,321},能排成的最小数字时321323

解法:利用sort函数,自定义compare方法。为了解决大数问题,将数字转为字符串。将两个字符串连接,由于两种连接方式生成的字符串位数相同,可以直接使用字符串比较函数strcmp,返回较小的那个组合。

 

46、【字符串/DP】把数字翻译成字符串

(0翻译成a,...,11翻译成l,...,25翻译成z),求出一串数字有几种翻译方法

例:12259,一共存在5种翻译方法,分别是1|2|2|5|8,12|2|5|8,12|25|8,1|22|5|8,1|2|25|8

解法:考虑倒序。用一个数组dp[i]保存i到末位的翻译数。如果i与i+1无法组合,则dp[i] = dp[i+1];如果i与i+1能够组合,则dp[i] = dp[i+1] + dp[i+2]

边界条件:取用dp[i+2]时,注意判断是否越界,如果越界,则dp[i] = dp[i+1] + 1

 

47、【DP】礼物最大价值

(棋盘格中有数字,从左上到右下,每次向左或向下移动,求路径和)

解法一:暴力。一个长度为n的字符串包含O(n^2)个子串,同时每个子串需要O(n)的时间判断是否包含重复字符,因此总的时间复杂度为O(n^3)

 

48、【DP】最长不含重复字符的子字符串

(返回最长子串长度)

解法一:暴力。一个长度为n的字符串包含O(n^2)个子串,同时每个子串需要O(n)的时间判断是否包含重复字符,因此总的时间复杂度为O(n^3)

解法二:函数f(i)表示【以第i个字符结尾】的不包含重复字符的子字符串的最长长度。分为3种情况:

    1、str[i]没有出现过时,f(i) = f(i - 1) + 1

    2、str[i]出现过且两次出现的间隔d大于f(i-1),表示str[i]上次出现的位置在当前不重复子串之外,没有影响,f(i) = f(i - 1) + 1

    3、str[i]出现过且两次出现的间隔d小于或等于f(i-1),表示str[i]上次出现的位置在当前不重复子串之内,需要更新当前不重复子串,f(i) = d

边界条件:字符串为空,

 

49、【其它】丑数

(能被1,2,3,5联合整除的数,求第1500个丑数)

解法一:暴力。3个循环,把2除尽,把3除尽,把5除尽,如果为1,则为丑数。缺点:每个数字都要除尽之后才能判断是不是丑数,因此在非丑数上会消耗大量时间

解法二:由于丑数肯定是由其他丑数乘以2,3,5构成,因此对于按顺序递增的丑数而言,新的丑数可以通过对前面已经求得的丑数进行乘法操作得到,3种乘数得到的新丑数中的最小值,就是顺序递增的丑数序列的最新值。还可以继续优化,用T2, T3, T5来表示丑数的【下界】,所谓下界,就是乘以2,3,5后,得出的值不会是之前记录过的(是新值)。T2, T3, T5需要在每轮计算后进行更新,可以避免多余的运算

边界条件:index = 1, index  < 1的无效输入

 

50、【字符串】字符串中第一个只出现一次的字符

解法一:暴力。时间复杂度O(n^2)

解法二:利用哈希表的思想。构建定长数组,将字符作为下标,存储该字符出现的次数。两次扫描,第一次扫描用于统计每个字符出现的次数,第二次扫描用于找到第一个出现次数为1的字符。时间复杂度O(n)+O(n) = O(n)

边界条件:字符串为空

 

51、【数组】数组中的逆序对

例:在数组{7,5,6,4}中,一共存在5个逆序对,分别是{7,6},{7,5},{7,4},{6,4},{5,4}

解法一:暴力。时间复杂度O(n^2)

解法二:归并思想。将数组按两个一组切分,记录包含两个元素数组的逆序对数,同时进行排序,避免后期重复计算。类比归并操作,进行4个一组的逆序对数量统计。两个指针分别指向两个数组的高位,判断指针所指元素的大小,如果是逆序(左边元素大于右边元素),累加逆序数,注意此时的逆序数等于右边起始位置到指针位置所有的元素个数(因为上一步已经在子数组中按递增排好序)。注意如果某个数组已经排完,剩下的元素可以直接放入(类比归并操作)。

边界条件:数组为空,数组只包含一个数字,数组只包含两个数字

 

52、【链表】两个链表的第一个公共节点

解法一:暴力,从一个链表出发,每到一个节点,就遍历另一个链表,判断是否是公共节点。时间复杂度O(mn),m、n为链表长度

解法二:两条链表同时出发。当其中一个链表到终点时,记录两个链表的步数差n。让长链表先走n步,再同时移动两条链表,相遇的点即为公共节点。时间复杂度O(m + n)

边界条件:二叉树只有左/右节点、输入的二叉树根节点为nulptr(空二叉树)

 

53.1、【数组】给定数字k在排序数组中出现的次数

解法一:排序数组,首先想到二分。通过二分找到其中一个k的位置,从该位置向左右扫描,直到扫描到边界。时间复杂度O(n)

解法二(优):解法一的时间复杂度可以继续优化。考虑通过二分查找,直接找到左右边界。以左边界为例,二分查找,直到满足(值==k 且 为左边界) 时,跳出循环,右边界同理。时间复杂度O(logn)

边界条件:k不存在,返回-1。 index-1可能越界,所以index == 0时,需要跳出循环

 

53.2、【数组】长度为n-1的递增排序数组,数字范围0~n-1,求缺失的那个数

(总共n个,实际只有n-1个,求缺失的那个数)

解法一:数学解法。先求出0~n-1的累加和 n * (n-1) / 2 ,再求出数组实际的累加和,相减,即为缺失的那个数。没有利用到排序的性质,时间复杂度O(n)

解法二(优):排序数组,首先想到二分。完整的数组中,数组下标对应的值,与下标是相等,以此作为判断条件,比较下标与对应值的大小关系,index < nums[index] && index-1 < nums[index],则继续找左边;index < nums[index] && index-1 == nums[index - 1], index -1即为所要找的值;index = nums[index],继续找右边。时间复杂度O(logn)

边界条件:数字合法性判断,n < 0,数组不是递增序列,输入数字不在 0~n-1范围内

 

53.3、【数组】递增数组,找到一个值与下标相等的数字

解法一:暴力扫一遍。时间复杂度O(n)

解法二(优):排序数组,首先想到二分。当index < nums[index]时,找左边;index > nums[index],找右边;直到 index  = nums[index]。时间复杂度O(logn)

边界条件:数组不是递增序列,数组中不存在这样的数字

 

54、【树】二叉搜索树的第K大节点

解法:中序遍历,按左子树,根节点,右子树的顺序查找,每次查找 k--,直到 k  == 1时,下一个要找的就是第K大节点 (此题编码不容易)

边界条件:k可能等与 0 或者 1, 二叉树只有左/右节点 ,输入的二叉树根节点为nulptr(空二叉树)

 

55.1、【树】二叉树的深度(最长路径)

解法:递归,先从左子树开始找,再从右子树开始找,返回左右子树中的最大深度 + 1

边界条件:二叉树只有左/右节点、输入的二叉树根节点为nulptr(空二叉树)

 

55.2、【树】判断是否是平衡二叉树(左右子树深度差不超过1)

解法一:仿照55.1,从根节点开始,递归计算每棵子树的深度,直到所有子树都满足平衡二叉树的定义

解法二(优):解法一让许多树节点的深度进行了重复计算,因此考虑后序遍历,每次都记录节点的深度,这样可以一边遍历一边判断每个节点是不是平衡点,避免重复计算

边界条件:二叉树只有左/右节点、输入的二叉树根节点为nulptr(空二叉树)

 

你可能感兴趣的:(Data,structure,and,algorithm)