本文内容为练习LeetCode题目时的解题思路和不同算法的记录,实现语言为C++,代码保存在Github,均已在LeetCode提交AC且均含最优解并尽多尝试多解,单击标题以查看,持续更新,暂时只进行Top Interview Questions列表下题目。
现在觉得全写在一篇文章里面过于臃肿且不易查找,我又不想做个目录出来,那样岂不是更臃肿?因此如果刷第二遍或者准备停止刷LeetCode的话我会把这篇记录按题目拆分出来,也许会根据专题整理一下多一级目录,也许不会,的文集内不能再分级实在是一个很不好的缺点。[2018.05.09]
2018.05.11 update:
378. Kth Smallest Element in a Sorted Matrix
#56/145 solved#
1. Two Sum
除了双重遍历外,可以用hash的方式空间换时间,遍历过程中依次存放每个元素的值及其下标,如果当前元素的互补数(target-nums[i])在哈希表中存在则可得结果。
7. Reverse Integer
反转32位整数,需要考虑溢出问题。
原本用了long long来判断溢出,但后来发现有个家伙用溢出后的数做逆操作与上次结果比较不同来判断,十分有才,判断方式:
if ((aux_ret - x % 10) / 10 != ret) return 0;
13. Roman to Integer
罗马数字的规则对左减数字有限制(仅限于I/X/C且不能跨位相减),但右加数字没有,因此逆序做转换是AC的但顺序转换会WA(eg:MCMXCVI-1996)。
14. Longest Common Prefix
求一组字符串的最长相同前缀(LCP),个人给出两种解决方案,一种是横向地顺序遍历每个字符串与之前得到的LCP计算新的LCP,这种思想可以划分到动态规划中;另外一种方法是考虑整体类似于一个不完全的字符矩阵,那么我们可以纵向地从第一列开始逐列对比,全部相同就把该字符加到LCP中,不同就可以返回LCP了。
此外LeetCode还给出了分治法和二分查找法的解决方案,思想也很简单,但是不如前面两种方法简洁也没有明显优势,因此了解即可。
20. Valid Parentheses
学过编译原理的应该都知道怎么做,可以利用栈来进行括号匹配,有无法匹配的括号那么说明语法错误。
21. Merge Two Sorted Lists
第一种迭代方案是在需要合并时交换当前节点和比较节点,也是自己第一时间想到的做法;
第二种迭代方案是按大小顺序依次合并两个链表的每个节点到新链表;
第三种是递归的做法,从第一个节点开始依次合并每个最小节点,注意此方法不是尾递归,不适用于过长链表合并,可能会导致栈溢出,但该题目目前的测试用例规模还没有到临界点,可以AC。
22. Generate Parentheses
穷举n对圆括号可以构成的所有合法字符序列。
第一个想到的方法是把每个合法字符串作为一行形成一个矩阵纵向遍历,每次添加一个括号,对于每行的字符串,如果下一个可能添加的括号有两种选择,则拷贝其一到矩阵最后一行;
第二个方法是递归回溯,可以把回溯过程看做一棵二叉树,每次递归调用会拷贝一次当前字符串然后判断下次添加的括号可能性(分支),直到n对括号全部添加完成后(叶子节点)把当前字符串加入结果集;
第三个方法LeetCode上称之为Closure number但我觉得像是分治法(还是DP?),这个方法比较trickier,LeetCode上的解释也没太明白,这里就说一下个人理解,我们可以把一个合法的括号字符串看做一个闭包,它必然起于左括号止于右括号,且每个合法括号串都可以从某个位置分为两个同样合法的括号串(包括空串),因此对于给定的n所可能构造的所有括号串都可以一步步划分为两个子闭包并解构(去掉头尾的一对左右括号)直到最小闭包——空串为止。基于以上分析,我们就可以从空串开始逆向一步步对两个子闭包之一构造新的闭包(在头尾各添加一个左/右括号)然后合并,至于构造对象是谁都是可以的,也就是代码中的核心部分:
ret.push_back(left + '(' + right + ')');// 以right闭包为构造对象
//ret.push_back('(' + left + ')' + right);// 或以left闭包为构造对象
26. Remove Duplicates from Sorted Array
一开始题意理解错误了..题目要求是在O(1)复杂度的前提下得到输入排序数组去重后的结果,注意要用输入数组保存去重后的数组,输出的长度是去重后数组的长度。顺便一提in-place算法一般多采用替代或者交换的方式来实现,我的解法是遍历整个数组依次把第n个不重复的元素替换数组下标为n-1的元素得到最终结果。
28. Implement strStr()
字符串匹配函数的实现,逐字匹配过于低效,用hash方式空间换时间。
也可以用KMP算法来实现,其本质是对于模式串中前后缀相同的子串减少匹配次数,回头实现了之后补充源码再梳理一下KMP的原理。
38. Count and Say
根据上一次的字符串计算下次的结果,迭代法,维持一个相同数字计数器,当遇到不同数字或者字符串结尾时把这个数字和计数器的值附加到新字符串后面。
46. Permutations
给定一个无重复整数数组,穷举其所有排列。
- 递归回溯
思路比较清晰,每次定位一个数字,锁上这个数字在的位置(剩余数字不可以排在这里),每次上锁后记录偏移量(这个数字的位置),然后排列剩下的数字,完成后根据偏移量插入之前定位的数字。当只剩一个数字未确定时(只有一种排列)开始层层回溯直到最终结果。
关键之处在于锁和偏移量的更改要想清楚才能保证回溯不会出错。 - 动态规划
与方法1异途同归,对于上一次获取到的所有排列,插入新的元素,新形成的排列数量为原排列数量乘以i(即阶乘i!),其状态转移方程如下:
dp[i][j] = dp[i - 1][j / (i + 1)].insert(dp[i - 1][j / (i + 1)].begin() + j % (i + 1), nums[count - i - 1]);
- 其中dp[i]表示新的排列集合,dp[i-1]表示上一次的排列集合,代码实现时分别用dp_next和dp_prev表示,j/(i+1)是选择插入对象,因为对一个排列插入新元素共有i+1种可能性,j%(i+1)是插入新元素的偏移量。
- 做完之后看了下discuss,发现自己没有很好地利用"distinct"这个条件,貌似还有个Permutations II是允许包含重复元素的输入来着,emmm,但是不觉得利用distinct的解法更好因此没有再写一个针对它的实现。
53. Maximum Subarray
从左到右遍历数组求和:
1.如果第i个元素前的和小于0,那么从该元素重新开始求和(否则不是加上前面的和反倒又变小了么,以防越过最大子数组左界);
2.如果本次求和的结果大于上次保存的最大子数组和,则更新后者为当前求和结果(防止越过最大子数组右界的情况)。
这是O(n)的解决方案,不过我一开始并没有意识到这个方法本质上其实是动态规划,根据动态规划的思想重新解释的话,步骤(1.)就是其状态转移方程
sum(vec,i) = vec[i] + (sum(vec,i-1) > 0 ? sum(vec,i-1) : 0)
而这只能不断更新确定子数组左界,有可能会越过右界,这时结果是不可靠的,例如[1,1,-3,0]这种情况,其最大子数组为[1,1],但当遍历到-3时,sum[i-1]=2而vec[i]=-3,导致sum[i]=-1,越过右界vec[1]=1,因此需要步骤(2.)来防止这种情况的发生,即对比上次求和结果与本次求和结果,如果变小了,那么有可能越过右界,不取其值。
and WHY using divide and conquer approach
?我并不认为分治法更subtle,不但麻烦且效率低。
66. Plus One
比较简单,模拟加法进位而且是特化的只+1版本,需要注意处理最高位进位的情况。
69. Sqrt(x)
求平方根,可以用二分法求也可以用牛顿法求。二分法不提,牛顿迭代法注意退出条件和整形溢出,另外整形相除的精度损失在计算顺序不同的情况下可能导致WA:
牛顿迭代法公式:xn+1=xn+f'(xn)/f(xn);
ret = (ret + x / ret) / 2;// AC 减少除法造成的精度损失
ret = ret / 2 + x / (ret * 2);// WA
70. Climbing Stairs
到达第n级台阶的方式有两种:从n-1阶跨1阶或从n-2阶跨2阶,因此dp[n]=dp[n-1]+dp[n-2]。可以用动态规划做也可以作为斐波那契数列做,但是暴力递归会导致TLE。
88. Merge Sorted Array
给定两个有序数组nums1和nums2,合并nums1的前m个和nums2的前n个元素到nums1中,nums1的长度不小于m+n(注意此处:需要抹除合并后多于m+n的部分,因为输出的结果是nums1)。 while (i2 > -1) nums1[i--] = i1 > -1 && nums1[i1] > nums2[i2] ? nums1[i1--] : nums2[i2--]; 二叉树中序遍历,递归方法自然是第一选择,但是要求使用迭代且空间复杂度为O(1),那么可以选择莫里斯遍历(Morris Traversal): 判断一个二叉树是否左右对称,问题可以转换为判断根节点的两个子树是否镜像对称,其特点是结点值相同,且左子树的左子树与右子树的右子树镜像对称、左子树的右子树与右子树的左子树镜像对称。 二叉树深度优先遍历。 对已排序数组建立平衡二叉树,递归思想,数据结构了解一下。 杨辉三角,就算没有了解的人也很容易发现其规律,每层左右边界均为1,其他数字为上一层对应两个数字之和。其实也是个动态规划问题,作为数组来归纳其状态转移方程就是 triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j] 维持一个最小买入价格和当前卖出利润来计算最大利润。注意此题不可以用双层循环,会导致TLE。 只有一股股票,在知晓给定天数每天价格的前提下获取最大利润,可以将其买卖多次但因为只有一股所以不可以买入复数股再卖掉。说实话感觉这题没什么意思,还不如13题的罗马数字转换。 判断回文字符串,但是要求大小写不敏感并且只关心字母和数字,逻辑比较清晰,只判断在az|AZ|0~9间的字符并把小写字符转换为大写字符即可。实现没有用库函数,用库函数判断是否阿尔法字符和进行大小写转换比较简单清晰一些。 异或的用法很亮眼,坦白地说我没想起来,很惭愧,位运算用到的比较少。 判断一个链表是否有环: 这道题还是比较麻烦的(在LeetCode上AC率15.2%倒数第三),一开始的基本思路是先获取所有构成的不重复直线,然后对每条直线遍历所有的点计算线上点数,但这样有三个问题,一是“不重复”的处理是很笨重,二是要处理大数相乘可能导致的int溢出,三是要处理int除法的精度损失。 实现一个可以获取最小值的栈,要求压出栈、获取栈顶和最小元素的操作时间复杂度都是O(1)。 emmm这道题...我想到了双指针“跑步”但是跑偏了→_→ 主要元素的数量多于一半的情况下意味着最多数量者即为解,且不同元素的数量必然少于相同元素数量。 可能是我英文水平不够,读了好几遍才理解这题意思,事实上就是完成26进制到10进制的转换。 这道题有点像脑筋急转弯...?好吧是我数字敏感性不够#_# 获取数组循环右移n步的结果。 位运算题,每次完成1bit的位置反转共32bit,要先想清楚运算逻辑。 位运算题,因为是求二进制位为1的数量,因此把n循环右移并与1做与(&)运算直到n为0即可(右移高位补0,为0时说明剩下的二进制位中已经没有1了)。 最先想到的还是递归,不过提交之后会TLE,没办法还是写迭代吧,为了不超时,又是一个动态规划的做法。 dp[i] = max(dp[i - 1], nums[i - 1] + dp[i - 2]) 对各数位的平方循环求和,如果结果为1返回true,结果发生了循环返回false(哈希确认),否则继续下一次数位平方求和。 求小于n的素数数量,筛法实现,假设所有(1,n)内的所有自然数都是素数,从2开始,如果是素数那么它的所有倍数都是非素数,遍历完成后可以筛出所有素数。 单链表反转的迭代和递归实现。 哈希或者set集合。 判断单链表是否回文。考虑O(n)时间复杂度和O(1)空间复杂度,因为回文链表的后半部分必然是前半部分的反转,有以下解决方案: 这道题很容易,把指向当前节点的指针指向下一节点即可,但是需要注意不要忘记内存管理,记得释放。 给定数组nums,对于其中的每一个元素nums[i],求出除nums[i]之外所有元素的乘积。 可以把前面和后面的部分分别视为两个因子front和back,遍历nums[i],分别从头/尾部迭代计算front/back,并分别在头/尾部与对应的ret[i]/ret[n-1-i]相乘,遍历完成后即得结果。 两种方法:排序后比较或哈希思想。 要满足时间复杂度O(n)和空间复杂度O(1),第一时间想起来的是求n个数的和然后减去实际和的方式(高斯公式),另外LeetCode上还有一种解法是用异或也很棒,再次用到了异或的交换律。 可以顺序遍历,把第n个发现的非零数与第n个数(0)交换,也可以逆序遍历,把第n个发现的0与第n个非零数交换,为了减少交换次数,可以在遍历次数等于n时不做交换。 一种迭代求解,一种换底公式,注意要以10为底,否则会有精度问题。LeetCode给出还有另外两种解法,进制转换和用满足条件的最大int对n求余。 回字的四种写法。 获取给定集合中出现频率最高的k个元素,要求时间复杂度低于O(nlogn),很经典的题目。 写了三种,哈希、遍历和排序,其中排序不适用于不改变交集元素出现顺序的要求(For follow up)。 每次相加分为两步,先计算无进位再计算进位,用了递归思想,逻辑比较清晰,因为ban掉了+-运算符,所以很容易想起来如何用位运算做。 给定一个横向和纵向都递增的n*n矩阵,找出其中第k小的元素。 这道题的意义在于去了解Fisher-Yates洗牌算法。 Fisher-Yates原理: 可以想到两种方案,一是哈希表保存字符计数信息,然后获得单字符index,二是两次遍历查找是否重复。 vector初始化以便使用下标。 暴力求解的时间复杂度过高O(n4),可以二分法解决,4个数组两两一组求和然后判断是否互补即可,复杂度为O(n2)。
个人有两种解法,第一种直接把nums2插入到nums1的第m个元素后面,擦除多余部分然后对nums1排序可得结果;第二种遍历nums1的前m个和nums2的前n个元素,对比大小按顺序插入到nums1中然后擦除多余部分,需要注意的是nums1的合法长度会随插入新元素数量增长(即循环条件之一应为i1
94. Binary Tree Inorder Traversal
二叉树遍历的主要问题在于当到达叶子节点时如何返回对应的父节点,而该方法的解决方法是通过额外的时间消耗和一个辅助指针,来将父节点对应的需要返回到它的叶子节点的空右子节点指针指向它(相当于构造了一个环),这样,当遍历到叶子节点需要返回时便可以通过该指针回到父节点继续下一步操作。
核心步骤如下:
101. Symmetric Tree
据此思想,递归的实现容易想到,迭代的做法我直接模拟了函数调用栈的方式来实现。104. Maximum Depth of Binary Tree
108. Convert Sorted Array to Binary Search Tree
118. Pascal's Triangle
121. Best Time to Buy and Sell Stock
122. Best Time to Buy and Sell Stock II
125. Valid Palindrome
136. Single Number
对本题来说,因为异或运算是满足交换律的,因此所有出现两次的数字最终异或结果为0,可以得出唯一一个仅出现一次的single number。141. Linked List Cycle
如果有环,那么必然有一个节点指向前面的某个节点,基于此想法可以用hash解决,但空间复杂度是O(n);
另外一种很妙的方案,是类比两个人在闭环跑道跑步,如果两人速度不同则必然会相遇,相似地,维持两个指针,一个每次“跑两步”(fast=fast->next->next),另一个每次“跑一步”(slow=slow->next)。如此当有环时两个指针必然会有重叠的情况,无环时指针最终会到达“终点”(null),退出循环。149. Max Points on a Line
后来选择了另一种处理方式,忽略问题一避免问题二,着重处理问题三,核心思想是对于一个已确定的点,只要其他的点与该点构成直线的斜率相同,那么便都在同一直线上(直线的点斜式思想)。步骤如下:
sp1. 对于重复点(锚点本身也算是一个重复的点)的数量进行累加,暂称重复数量;
sp2. 对于斜率不存在的情况,通过std::pair的一对int均置为0来区分为单独一组(除数不可能为0);155. Min Stack
因为是栈,所以出栈元素的顺序是固定的(先入后出),那么可以维持一个容器保存栈中最小值更新的记录,我这里用了一个数组,如果压/出栈时最小值更新了,那么把更新的最小值放入/取出数组,这样无论何时该数组的最后一个元素都是当前最小值。关键代码:if (mins_.empty() || x <= mins_.back()) mins_.push_back(x);// 压栈时的判断
if (values_.back() == mins_.back()) mins_.pop_back();// 出栈时的判断
160. Intersection of Two Linked Lists
要求找出两个无环单链表的交叉点,时间复杂度O(n),空间复杂度O(1),不能改变原链表结构。假如把两个链表拼接起来,那么如果有交叉点则必然在同一时间达到这个交叉点,如下图所示:
169. Majority Element
171. Excel Sheet Column Number
172. Factorial Trailing Zeroes
题目是求出n!结果中数位0的个数,但要求时间复杂度为对数阶。其中原理是结果中的每个0都必然来自于x10,而10可以分解为2x5,由于因数2的数量多于因数5,因此我们只需要统计出n!的因数中5的数量即为结果中数位0的数量,至于求因数5的数量可以使n对5辗转相除,累加每次相除的结果(e.g. 26/5=5,5/5=1,5+1=6,26的阶乘可以分解出6个因数5,则其阶乘结果中含6个0数位):while ((n /= 5) > 0) sum += n;
189. Rotate Array
根据右移步数和数组长度可以知道每个数组元素的最终位置,通过右移步数对数组长度求余,得到原本数组尾部移动到数组头部的元素数量,然后把这些元素插入头部再清除尾部多余元素。190. Reverse Bits
191. Number of 1 Bits
198. House Robber
因为不能抢劫两栋相邻房屋,因此抢劫到第n栋房屋时的最大收获要么是前面n-1栋的最大收获,要么是前面n-2栋的最大收获加上第n栋的收获,状态转移方程:
202. Happy Number
204. Count Primes
206. Reverse Linked List
217. Contains Duplicate
234. Palindrome Linked List
237. Delete Node in a Linked List
238. Product of Array Except Self
简单来说,返回数组ret中:ret[i] = nums[0]*nums[1]*...*nums[i-1]*nums[i+1]*...*nums[n-1];
ret[i] = (nums[0]*nums[1]*...*nums[i-1]) * (nums[i+1]*...*nums[n-1]);
ret[i] = 1 * (nums[0]*nums[1]*...*nums[i-1]) * (nums[i+1]*...*nums[n-1]);// 1*front*back
242. Valid Anagram
268. Missing Number
283. Move Zeroes
326. Power of Three
这道题没什么意义,踩。344.Reverse String
347. Top K Frequent Elements
皆满足题目要求,受益匪浅,之前还不晓得有个nth_element来着。350. Intersection of Two Arrays II
关于Follow up第三条,如果两个数据量都是海量的,可以采取外排序方式然后对排序后的数组求交集(方法三)。371. Sum of Two Integers
378. Kth Smallest Element in a Sorted Matrix
实现了两种方法,堆排和快排,其中快排没有用std的算法权当练手了,可以参照347题,那道题比这个复杂一些,我给出了较为详细的解释。
另外还有一种据说时间复杂度是O(n)的特化算法但是我没仔细看,后续有时间了再学习一下。384. Shuffle an Array
对于给定的一个长度为n原始序列,每次(第i次,i>=0)随机选择原始序列前n-i个元素中的一个与第n-i个元素交换位置,即从后往前每次确定一个元素,直到全部确定完成shuffle。
只要随机选择元素的时候是真随机,那么shuffle结果也是真随机的。387. First Unique Character in a String
412. Fizz Buzz
454. 4Sum II