leetcode-ThirdWeek
本文为记录自己学习算法的过程
[70]爬楼梯
假设你正在爬楼梯。需要n
阶你才能到达楼顶。
每次你可以爬1
或2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
1 | 输入:n = 2 |
示例 2:
1 | 输入:n = 3 |
提示:
- $1 <= n <= 45$
题解:
1 | class Solution { |
思路:
这题主要考察的就是思维能力是动态规划吧,在思考的时候不能总是固定着自己的思想,总是想着下一步可以走一步或者两步,一般都会考虑直接递归,但是在这题会导致超时,所以需要重新考虑方法。
在这题主要的思路就是想清楚考虑好走台阶的真正意思。在这题中我们可以分析发现走第n
个台阶可以由第n-1
个台阶走上来,也可以由n-2
个台阶走上来,所以我们直接计算好上1
个台阶有几种走法,第2
个台阶有几种走法,后面的第n
级台阶的走法就是n-1
级台阶的走法加上n-2
级台阶的走法。
[83]删除排序链表中的重复元素
给定一个已排序的链表的头head
,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
示例 1:
1 | 输入:head = [1,1,2] |
示例 2:
1 | 输入:head = [1,1,2,3,3] |
提示:
- $链表中节点数目在范围 [0, 300] 内$
- $-100 <= Node.val <= 100$
- $题目数据保证链表已经按升序 排列$
题解:
1 | class Solution { |
思路:
这题就是简单的将重复元素去重,这题最大的特点是链表的元素已经是排序好了的,所以要利用这个特点来解题。
思路就是遍历链表,让一个指针p
指向链表第一个元素,然后向后遍历,判断当前指向的元素的值与下一个元素的值是否相等,相等就让当前这个元素的next
域指向next->next
,这样就相当于去掉了重复的元素,这里需要注意的是最好在断开节点的时候,将断开的那个节点释放资源。还有一点就是在更新操作之后不要马上向后移动节点,因为更新操作了,可能导致移动指针指向的是空节点,再判断空节点的后一个域会出现异常。
LeetCode题解
方法一:一次遍历
思路与算法
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。
具体地,我们从指针 $\textit{cur}$ 指向链表的头节点,随后开始对链表进行遍历。如果当前 $\textit{cur}$ 与 $\textit{cur.next}$对应的元素相同,那么我们就将 $\textit{cur.next}$ 从链表中移除;否则说明链表中已经不存在其它与 $\textit{cur}$ 对应的元素相同的节点,因此可以将 $\textit{cur}$ 指向 $\textit{cur.next}$。
当遍历完整个链表之后,我们返回链表的头节点即可。
细节
当我们遍历到链表的最后一个节点时,$\textit{cur.next}$ 为空节点,如果不加以判断,访问 $\textit{cur.next}$ 对应的元素会产生运行错误。因此我们只需要遍历到链表的最后一个节点,而不需要遍历完整个链表。
代码
注意下面 $\texttt{C++}$ 代码中并没有释放被删除的链表节点的空间。如果在面试中遇到本题,读者需要针对这一细节与面试官进行沟通。
1 | class Solution { |
[88]合并两个有序数组
给你两个按非递减顺序排列的整数数组nums1
和nums2
,另有两个整数m
和n
,分别表示nums1
和nums2
中的元素数目
请你合并nums2
到nums1
中,使合并后的数组同样按非递减顺序排列
注意:最终,合并后数组不应由函数返回,而是存储在数组nums1
中。为了应对这种情况,nums1
的初始长度为m + n
,其中前m
个元素表示应合并的元素,后n
个元素为0
,应忽略。nums2
的长度为n
。
示例 1:
1 | 输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 |
示例 2:
1 | 输出:[1] |
示例 3:
1 | 输入:nums1 = [0], m = 0, nums2 = [1], n = 1 |
提示:
- $nums1.length == m + n$
- $nums2.length == n$
- $0 <= m, n <= 200$
- $1 <= m + n <= 200$
- $-10^9 <= nums1[i], nums2[j] <= 10^9$
题解:
1 | class Solution { |
思路:
这题就是考察双指针问题,拿到题目我们就可以想到就是遍历两个数组,将数组中的元素进行比较放入恰当的位置即可。唯一不同的就是我们需要将所有的元素都放入数组nums1
。
思路就是首先判断nums2
数组的长度是否等于0
,等于0
说明数组num2
没有元素,直接返回就可以了。如果num1
数组长度等于0
此时就可以直接将数组num2
里面的元素全部按顺序放进nums1
里面不需要考虑数组nums2
里面有没有元素。接着就是遍历两个数组,一开始都指向有效元素的位置,比较元素的大小,大的元素就插入到数组nums1
中,插入的位置为如下公式:
判断结束的条件是j>=0&&i>=0
因为当j<0
时表示已经将nums2
数组中的元素都放入nums1
中,而i<0
则表示nums1
数组中的元素都放在了恰当的位置,所以最后来个while
将nums2
数组中的元素放入nums1
中就可以了。
LeetCode题解
方法一:直接合并后排序
算法
最直观的方法是先将数组 $\textit{nums}_2$放进数组 $\textit{nums}_1$的尾部,然后直接对整个数组进行排序。
1 | class Solution { |
方法二:双指针
算法
方法一没有利用数组 $\textit{nums}_1$ 与 $\textit{nums}_2$ 已经被排序的性质。为了利用这一性质,我们可以使用双指针方法。这一方法将两个数组看作队列,每次从两个数组头部取出比较小的数字放到结果中。如下面的动画所示:
我们为两个数组分别设置一个指针 $p_1$ 与 $p_2$ 来作为队列的头部指针。代码实现如下:
1 | class Solution { |
方法三:逆向双指针
算法
方法二中,之所以要使用临时变量,是因为如果直接合并到数组 $\textit{nums}_1$ 中,$\textit{nums}_1$ 中的元素可能会在取出之前被覆盖。那么如何直接避免覆盖 $\textit{nums}_1$ 中的元素呢?观察可知,$\textit{nums}_1$ 的后半部分是空的,可以直接覆盖而不会影响结果。因此可以指针设置为从后向前遍历,每次取两者之中的较大者放进 $\textit{nums}_1$ 的最后面。严格来说,在此遍历过程中的任意一个时刻,$\textit{nums}_1$ 数组中有 $m-p_1-1$ 个元素被放入 $\textit{nums}_1$ 的后半部,$\textit{nums}_2$ 数组中有 $n-p_2-1$ 个元素被放入 $\textit{nums}_1$ 的后半部,而在指针 $p_1$ 的后面,$\textit{nums}_1$ 数组有 $m+n-p_1$ 个位置。由于
等价于
永远成立,因此 $p_1$ 后面的位置永远足够容纳被插入的元素,不会产生 $p_1$ 的元素被覆盖的情况。
实现代码如下:
1 | class Solution { |
[94]二叉树的遍历
给定一个二叉树的根节点root
,返回它的中序遍历
示例 1:
1 | 输入:root = [1,null,2,3] |
示例 2:
1 | 输入:root = [] |
示例 3:
1 | 输入:root = [1] |
提示:
- $树中节点数目在范围 [0, 100] 内$
- $-100 <= Node.val <= 100$
进阶:递归算法很简单,你可以通过迭代算法完成吗?
题解:
方法一:递归
1 | class Solution { |
方法二:迭代
1 | class Solution { |
思路:
这道题就是二叉树的基本操作,具体可以查看我的文章二叉树
LeetCode题解
方法一和方法二类似就不实现了。
方法三:Morris 中序遍历
思路与算法
Morris 遍历算法是另一种遍历二叉树的方法,它能将非递归的中序遍历空间复杂度降为 $O(1)$。
Morris 遍历算法整体步骤如下(假设当前遍历到的节点为 $x$):
- 如果 $x$ 无左孩子,先将 $x$ 的值加入答案数组,再访问 $x$ 的右孩子,即 $x = x.\textit{right}$。
- 如果 $x$ 有左孩子,则找到 $x$ 左子树上最右的节点(即左子树中序遍历的最后一个节点,$x$ 在中序遍历中的前驱节点),我们记为 $\textit{predecessor}$。根据 $\textit{predecessor}$ 的右孩子是否为空,进行如下操作。
- 如果 $\textit{predecessor}$ 的右孩子为空,则将其右孩子指向 xx,然后访问 xx 的左孩子,即 $x = x.\textit{left}$。
- 如果 $\textit{predecessor}$ 的右孩子不为空,则此时其右孩子指向 $x$,说明我们已经遍历完 $x$ 的左子树,我们将$\textit{predecessor}$ 的右孩子置空,将 $x$ 的值加入答案数组,然后访问 $x$ 的右孩子,即 $x = x.\textit{right}$。
- 重复上述操作,直至访问完整棵树。
其实整个过程我们就多做一步:假设当前遍历到的节点为 $x$,将 $x$ 的左子树中最右边的节点的右孩子指向 $x$,这样在左子树遍历完成后我们通过这个指向走回了 $x$,且能通过这个指向知晓我们已经遍历完成了左子树,而不用再通过栈来维护,省去了栈的空间复杂度。
1 | class Solution { |
[118]杨辉三角
给定一个非负整数numRows
,生成「杨辉三角」的前numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
1 | 输入: numRows = 5 |
示例 2:
1 | 输入: numRows = 1 |
提示:
- $1 <= numRows <= 30$
题解:
1 | class Solution { |
思路:
这题主要考察的就是杨辉三角,所以只需要了解杨辉三角的基本性质就可以,这题目的不同的区别就是将杨辉三角的每行的数据放入vector
容器里面。
我的主要的思路就是根据给定的行数来生成好杨辉三角,然后题目给定了numRows
。遍历numRows
,每次将每行里面的元素放入一个vector
容器,这个vector
再放入需要返回的vector
即可。其实这题最主要的就是要知道杨辉三角的性质
- $当列数 j == 1时或者和行数 i == j列数 j 时,杨辉三角的值为 1$
- $其他的 a [ i ][ j ] = a [ i -1 ] [ j -1 ] + a [ i -1 ] [ j ] $
LeetCode题解
方法一:数学
思路及解法
杨辉三角,是二项式系数在三角形中的一种几何排列。它是中国古代数学的杰出研究成果之一,它把二项式系数图形化,把组合数内在的一些代数性质直观地从图形中体现出来,是一种离散型的数与形的结合。
杨辉三角具有以下性质:
每行数字左右对称,由 $1$ 开始逐渐变大再变小,并最终回到 $1$。
第 $n$ 行(从 $0$ 开始编号)的数字有 $n+1$ 项,前 $n$ 行共有 $\frac{n(n+1)}{2}$ 个数。
第 $n$ 行的第 $m$ 个数(从 $0$ 开始编号)可表示为可以被表示为组合数 $\mathcal{C}(n,m)$,记作 $\mathcal{C}_n^m$或 $\binom{n}{m}$,即为从 $n$ 个不同元素中取 $m$ 个元素的组合数。我们可以用公式来表示它:$\mathcal{C}_n^m=\dfrac{n!}{m!\times (n-m)!}$。
每个数字等于上一行的左右两个数字之和,可用此性质写出整个杨辉三角。即第 $n$ 行的第 $i$ 个数等于第 $n-1$ 行的第 $i-1$ 个数和第 $i$ 个数之和。这也是组合数的性质之一,即:
$(a+b)^n$ 的展开式(二项式展开)中的各项系数依次对应杨辉三角的第 $n$ 行中的每一项。
依据性质 $4$,我们可以一行一行地计算杨辉三角。每当我们计算出第 $i$ 行的值,我们就可以在线性时间复杂度内计算出第 $i+1$ 行的值。
1 | class Solution { |
- 感谢你赐予我前进的力量