力扣算法题思路解析(精选200题)
刷题思路见这篇文章https://mp.weixin.qq.com/s/xr2abGNv8wDZJ-qyN4KewQ
题目见https://leetcode-cn.com/list/6db2mu6
1.Two Sum(LeetCode 1)
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
//由于数组中每个元素只能用一次, 因此第一个元素下标取值0~n-1,第二个元素j取值i+1~n。
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> vec;
for(int i=0; i<nums.size()-1; ++i){
for(int j=i+1; j<nums.size(); ++j){
if(nums[i]+nums[j]==target){
vec.push_back(i);
vec.push_back(j);
return vec;
}
}
}
return vec;
}
优化:哈希映射解法(java):
public int[] twoSum(int[] nums, int target) {
// 由于哈希查找的时间复杂度为O(1),所以可以利用哈希容器 map 降低时间复杂度为O(n)
Map<Integer, Integer> map = new HashMap<>(); // key存数组值,value存数组下标
for(int i = 0; i < nums.length; ++i) {
if(map.containsKey(target - nums[i])){
return new int[]{map.get(target - nums[i]), i};
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
2. Add Two Numbers(LeetCode 2)
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
输入:l1 = [9,9], l2 = [3]
输出:[2,0,1]
//不补零,若链表不为空则用 sum(代表每个位的和的结果)加上,考虑进位。
//不要用ListNode* h = head->next; h = new ListNode(x);
//而要用LIstNode* h = head; h->next = new ListNode(x);
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* head = new ListNode(-1);//头结点
ListNode* h = head;//新链表的移动指针
int sum = 0;//1、记录每位的加和
bool carry = false;//2、判断每位是否有进位
while(l1 != nullptr || l2 != nullptr){
sum = 0;
if(l1 != nullptr){
sum += l1->val;
l1 = l1->next;
}
if(l2 != nullptr){
sum += l2->val;
l2 = l2->next;
}
if(carry){//3、如果有上一位传来的进位,则当前sum++
sum++;
}
h->next = new ListNode(sum % 10);
h = h->next;
carry = sum>=10 ? true : false;
}
if(carry){//4、如果最高位相加还有进位
h->next = new ListNode(1);
}
return head->next;
}
Java解法:
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode pre = new ListNode(-1);
ListNode cur = pre;
int sum = 0; // 1、记录每位的加和
boolean carry = false; // 2、判断每位是否有进位
while(l1 != null || l2 != null ) {
sum = 0;
if(l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if(l2 != null) {
sum += l2.val;
l2 = l2.next;
}
if(carry) { // 3、如果有上一位传来的进位,则当前sum++
sum++;
}
cur.next = new ListNode(sum % 10);
cur = cur.next;
carry = (sum >= 10) ? true : false;
}
if(carry) { // 4、如果最高位相加还有进位
cur.next = new ListNode(1);
}
return pre.next;
}
3.无重复字符的最长子串(LeetCode 3)
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
这道题主要用到思路是:滑动窗口
什么是滑动窗口?其实就是一个队列,比如例题中的 abcbcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 b,队列变成了 abcb,这时候不满足要求。所以,我们要移动这个队列!
如何移动?我们只要把队列的左边的元素移出就行了,直到满足题目要求(即第一趟while移出a,第二趟while移出b)。一直维持这样的队列,找出队列出现最长的长度时候,求出解!时间复杂度:O(n)
注意使用while循环而非if是窗口的左界右移的关键。
int lengthOfLongestSubstring(string s) {
if(s.size() == 0){
return 0;
}
int left = 0;//1、滑动窗口左界为left,右界为for循环的i。
int maxStr = 0;2、用于记录最大不重复子串的长度
unordered_set<char> lookup;
for(int i = 0; i < s.size(); ++i){
while(lookup.find(s[i]) != lookup.end()){//3、若当前元素在set中则进入while循环。
//使用while而非if是为了应对abb这种情况:访问到第二个b时,第一次while移出set中的a,第二次while移出set中的b。
//4、清除set中左界处对应元素,再左界右移一个单位
lookup.erase(s[left]);
left++;
}
maxStr = max(maxStr, i-left+1);
lookup.insert(s[i]);
}
return maxStr;
}
Java解法:
public int lengthOfLongestSubstring(String s) {
if(s.length() == 0) return 0;
int left = 0; //1、滑动窗口左界为left,右界为for循环的i。
int maxStr = 0; // 2、用于记录最大不重复子串的长度
Map<Character, Integer> map = new HashMap<>(); // key为字符,value为数组下标
for(int i = 0; i < s.length(); ++i) {
if( map.containsKey(s.charAt(i)) ) {
left = Math.max( left, map.get(s.charAt(i))+1 ); // 3、精髓,决定左界移动到哪
}
map.put(s.charAt(i), i); // 4、不管是否更新left,都要更新 s.charAt(i) 的位置。如果该key已存在,则会更新其映射的value
maxStr = Math.max(maxStr, i-left+1);
}
return maxStr;
}
4. 寻找两个正序数组的中位数(LeetCode 4)
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
方法一:数组归并。Tn = O(m+n)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int len = m + n;
int pre = -1; // 保存上一次循环的结果
int cur = -1; // 保存当前循环的结果
// 1、退出for循环时:若len为奇数则中位数为cur,若len为偶数则中位数为(pre+cur)/2.0
int p1 = 0; ///2、指向数组当前访问位置的指针
int p2 = 0;
for(int i = 0; i <= len/2; ++i) { //3、无论总长度为奇数还是偶数,都需要int(len/2)+1 次遍历
pre = cur;
if(p1 < m && ( p2 >= n || nums1[p1] < nums2[p2] ) ) { // 4、如果nums1未遍历完 且 (nums2已遍历完 或 nums1第一个值小于nums2第一个值)
cur = nums1[p1++];
}
else {
cur = nums2[p2++];
}
}
if(len % 2 == 0) { // 若len为偶数
return (pre + cur) / 2.0;
}
else { // 若len为奇数
return (double)cur;
}
}
}
方法二:二分查找。Tn = O(log(m+n))
4.最长回文子串 (LeetCode 5)
给你一个字符串 s
,找到 s
中最长的回文子串。
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
- 以下解法中「暴力算法」是基础,「动态规划」必须掌握,「中心扩散」方法要会写;「Manacher 算法」仅用于扩宽视野
方法一:暴力匹配 (Brute Force)
根据回文子串的定义,枚举所有长度大于等于 22 的子串,依次判断它们是否是回文;
bool valid(string s, int left, int right){
//判断传入的字符串是否回文
while(left < right){
if(s[left] != s[right]){
return false;
}
left++;
right--;
}
return true;
}
string longestPalindrome(string s) {
int size = s.size();
if(size < 2){
return s;
}
int maxLen = 1;
string res = s.substr(0,1);//从下标0开始的1个字符串
// 枚举所有长度大于等于 2 的子串
for(int i = 0; i < size-1; ++i){
for(int j = i+1; j < size; ++j){
if(j-i+1 > maxLen && valid(s,i,j) ){
maxLen = j-i+1;
res = s.substr(i,maxLen);
}
}
}
return res;
}
方法二:中心扩散法
枚举可能出现的回文子串的“中心位置”,从“中心位置”尝试尽可能扩散出去,得到一个回文串。
因此中心扩散法的思路是:遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。
枚举“中心位置”时间复杂度为 O(N)O(N),从“中心位置”扩散得到“回文子串”的时间复杂度为 O(N)O(N),因此时间复杂度可以降到 O(N^2)。
string longestPalindrome(string s) {
int size = s.size();
if(size < 2){
return s;
}
int left = 0, right = 0;
int len = 1;//记录每次循环的回文子串长度
int maxLen = 0;//记录当前最长的回文子串长度
int maxStart = 0;//记录最长回文子串的起始位置,避免每次循环都截取子串
for(int i = 0; i < size; ++i){
left = i - 1;
right = i + 1;
//注意:使用while循环而非if,因为每个中心点可能需要多次扩散。
while(left >= 0 && s[left] == s[i]){//1、如果中心点和左边点相等,则向左扩散。如abba中i指向第三位的b时。
left--;
len++;
}
while(right < size && s[right] == s[i]){//2、如果中心点和右边点相等,则向右扩散。如abba中i指向第二位的b时。
right++;
len++;
}
while(left >= 0 && right < size && s[right] == s[left]){//3、对齐后,如果左边点和右边点相等,则向两边同时扩散。如aba。
left--;
right++;
len += 2;
}
if(len > maxLen){//4、更新当前的最长回文子串
maxLen= len;
maxStart = left;//记录最长回文子串的起始位置
}
len = 1;
}
return s.substr(maxStart+1, maxLen);//5、边界扩散时多移动了一步。
}
5.整数反转(LeetCode 7)
给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1] ,就返回 0。
假设环境不允许存储 64 位整数(有符号或无符号)。
输入:x = 123
输出:321
int reverse(int x) {
long n = 0;
while(x != 0){
n = n * 10 + x % 10;//用x%10来得到末尾数字
x = x / 10;
}
return (n > INT_MAX || n < INT_MIN) ? 0 : n;//溢出判断
}
6.回文数(LeetCode 9)
给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。
方法一:暴力解法
将数字转化为字符串,再判断字符串与其反转字符串是否相等,或用判断回文字符串的方法来判断字符串是否回文。
bool isPalindrome(int x) {
string str = to_string(x);
reverse(str.begin(),str.end());
return to_string(x) == str;
}
方法二:进阶数字解法
通过取整和取余操作获取整数中对应的数字进行比较。
举个例子:1221 这个数字。
通过计算 1221 / 1000, 得首位1
通过计算 1221 % 10, 可得末位 1
进行比较
再将 22 取出来继续比较
bool isPalindrome(int x) {
if(x < 0){
return false;
}
int div = 1;//1、用于求最高位的除数,如1221的div为1000
while(x / div >= 10){
div *= 10;
}
while(x > 0){
int left = x / div;//最高位数字
int right = x % 10;//最低位数字
if(left != right){//2、判断最高位和最低位数字是否相等
return false;
}
x = (x % div) / 10;//3、去掉x的最高位和最低位数字
div /= 100;//由于去除了两位数字,故除数减小100倍
}
return true;
}
7.盛最多水的容器(LeetCode 11)
给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
暴力法:两重for循环枚举所有可能的状态,找出面积最大值,O(n^2),会超时。
双指针法:
算法流程:设置双指针 i,j 分别位于容器壁两端,根据规则移动指针(后续说明),并且更新面积最大值 res
,直到 i == j
时返回 res
。
- 设每一状态下水槽面积为 S(i,j),(0<=i<j<n),由于水槽的实际高度由两板中的短板决定,则可得面积公式 S(i,j)=min(h[i],h[j])×(j−i)。
-
在每一个状态下,无论长板或短板收窄 1 格,都会导致水槽 底边宽度 −1:
若向内移动短板,水槽的短板 min(h[i],h[j])可能变大,因此水槽面积 S(i,j) 可能增大。
若向内移动长板,水槽的短板 min(h[i],h[j]) 不变或变小,下个水槽的面积一定小于当前水槽面积。 -
通俗的讲,我们每次向内移动短板,所有的消去状态都不会导致丢失面积最大值 。
int maxArea(vector<int>& height) {
int i = 0, j = height.size()-1, res = 0;
//我们每次向内移动短板,所有的消去状态都不会导致丢失面积最大值 。
while(i < j){
if(height[i] < height[j]){//左指针为短板
res = max(res, height[i] * (j - i));
i++;
}
else{//右指针为短板
res = max(res, height[j] * (j - i));
j--;
}
}
return res;
}
8. 最长公共前缀(LeetCode 14)
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""
。
输入:strs = ["flower","flow","flight"]
输出:"fl"
方法一:纵向扫描:从前往后遍历所有字符串的每一列,比较相同列上的字符是否相同,如果相同则继续对下一列进行比较,如果不相同则当前列不再属于公共前缀,当前列之前的部分为最长公共前缀。时间复杂度O(mn)。
string longestCommonPrefix(vector<string>& strs) {
//纵向扫描
if(strs.size() == 0){
return "";
}
int length = strs[0].size();//第一个字符串的长度(横向长度)
int count = strs.size();//字符串的个数(纵向长度)
for(int i = 0; i < length; ++i){
char c = strs[0][i];//依次比较每一行的第i个字符
for(int j = 1; j < count; ++j){
if(i == strs[j].size() || strs[j][i] != c){//走到了第j行的末尾 或 第j行的字符与前行对应字符不相等。
return strs[0].substr(0,i);
}
}
}
return strs[0];//第一行为最长公共前缀
}
方法二:横向扫描 :两两比较字符串得到其最长公共前缀prefix,再将prefix与下一字符串比较得到新的prefix,直到字符串数组结束。时间复杂度O(mn),m是字符串平均长度,n是字符串个数。
string longestCommonPrefix(vector<string>& strs) {
//字符串数组中两两横向比较
int count = strs.size();
if(count == 0){
return "";
}
string prefix = strs[0];//当前的最长公共前缀
for(int i = 1; i < count; ++i){
prefix = twoLongestCommonPrefix(prefix, strs[i]);//两两比较得到新的最长公共前缀
if(prefix.size() == 0){//若前i个字符串已无公共子串,则返回空串。
break;
}
}
return prefix;
}
string twoLongestCommonPrefix(const string& str1, const string& str2) {//两两比较得到其最长公共前缀
int length = min(str1.size(), str2.size());
int index = 0;
while(index < length && str1[index] == str2[index]){
index++;
}
return str1.substr(0,index);
}
9.三数之和 (LeetCode 15)
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。注意:答案中不可以包含重复的三元组。
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
排序 + 双指针
本题的难点在于如何去除重复解。
算法流程:
1.特判,对于数组长度 n,如果数组为 null 或者数组长度小于 3,返回 []。
2.对数组进行排序。
3.遍历排序后数组:
- 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回结果。
- 对于重复元素(nums[i]==nums[i-1]):跳过,避免出现重复解
- 令左指针 L=i+1,右指针 R=n-1,当 L<R 时,执行循环:
- 当 nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,R 移到下一位置,寻找新的解
- 若和大于 0,说明 nums[R]太大,R 左移
- 若和小于 0,说明nums[L] 太小,L 右移
复杂度分析:
时间复杂度:O(n^2)。数组排序O(nlogn),遍历数组O(n),双指针遍历O(n),总体O(nlogn)+O(n)*O(n)
空间复杂度:O(1)。
vector<vector<int>> threeSum(vector<int>& nums) {
int size = nums.size();
vector< vector<int> > res;
if(size < 3){ // 1.特判,直接返回。
return res;
}
sort(nums.begin(),nums.end()); // 2.排序
int target = 0;
for(int i = 0; i < size; ++i){
if(nums[i] > target){ // 3.1因为已经排序好,所以后面不可能再有三个数相加等于0(仅target>=0时才可剪枝)
return res;
}
if(i > 0 && nums[i] == nums[i-1]){ // 3.2对于重复的i位置元素跳过,避免出现重复解(精髓)
continue;
}
int L = i+1, R = size-1;
while(L < R) {
if(nums[i]+nums[L]+nums[R] == target){ // 3.3.1找到目标值
res.push_back( {nums[i],nums[L],nums[R]} );
// 相同的L和R不应该再次出现,因此跳过
while(L < R && nums[L] == nums[L+1]){
L++;
}
while(L < R && nums[R] == nums[R-1]){
R--;
}
// 并同时将 L,R 移到下一位置,寻找i位置新的解
L++;
R--;
}
else if(nums[i]+nums[L]+nums[R] > target){// 3.3.2大于目标值
R--;
}
else{ //nums[i]+nums[L]+nums[R] < 0 // 3.3.3小于目标值
L++;
}
}
}
return res;
}
10.最接近的三数之和(LeetCode 16)
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
排序+双指针:
时间复杂度:排序O(nlogn)+位置i遍历数组O(n)*双指针遍历O(n)=O(n^2)。
int threeSumClosest(vector<int>& nums, int target) {
//假定数组元素个数大于等于3
sort(nums.begin(), nums.end());
int size = nums.size();
int closeSum = nums[0]+nums[1]+nums[2];
for(int i = 0; i < size; ++i) {
int L = i+1, R = size-1;
while(L < R) {
int curSum = nums[i]+nums[L]+nums[R];
if(abs(curSum-target) < abs(closeSum-target)){ // 更新最近距离
closeSum = curSum;
}
// i位置下的左右边界移动
if(curSum > target){
R--;
}
else if(curSum < target){
L++;
}
else { //curSum == target,不可能有更小的最近距离了,则提前返回
return closeSum;
}
}
}
return closeSum;
}
11.电话号码的字母组合(LeetCode 17)
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
方法一:回溯法(暴力法且无需剪枝)
一次回溯访问分支树的一个顶点,如a、d、e、f、b...
时间复杂度:O(3^m * 4^n),其中m是输入中对应3个字母的数字个数,n是输入中对应4个字母的数字个数。空间复杂度O(m+n),来源于递归调用栈。
vector<string> res;
vector<string> numberToLetter = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//字符表
vector<string> letterCombinations(string digits) {
//回溯法(类似树的分支,一次回溯即从根节点到叶节点的一次检索)
if(digits.size() == 0){
return {};
}
backTrack(digits, {}, 0);// 一次回溯访问分支树的一个结点
return res;
}
void backTrack(string digits, string str, int index) { // str表示一次检索形成的字符串。index表示当前分支已遍历的字符个数。
if(digits.size() == index){ // 回溯条件,保证走完从根节点到叶节点的一个分支
res.push_back(str); // 将一种结果压入res
return;
}
else {
int pos = digits[index] - '0'; // 获取digits中第index个字符,char型作差。
string temp = numberToLetter[pos]; // 获取下标pos对应的字符串,如2对应的"abc"
for(int i = 0; i < temp.size(); ++i) {
backTrack(digits, str+temp[i], index+1);// 进行下一层迭代,注意同一层迭代时不改变str和index等参数值
}
}
}
12.四数之和(LeetCode 18)
给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:答案中不可以包含重复的四元组。
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
思路:与前面三数之和的思路几乎是一样,使用两重循环+双指针,a在最外层循环,里面嵌套b循环,再嵌套双指针c,d包夹求解。
时间复杂度:排序O(nlogn)+两重循环O(n^2)*双指针O(n)=O(n^3)
vector<vector<int>> fourSum(vector<int>& nums, int target) {
int size = nums.size();
vector<vector<int>> res;
if(size < 4) return res;// 1.特判
sort(nums.begin(), nums.end()); // 2.排序
for(int i = 0; i < size-3; ++i) {
//if(nums[i] > target) return res;// 剪枝(仅target>=0时才可剪枝)
if(i>0 && nums[i] == nums[i-1]) continue; // 3.1避免i位置重复解
for(int j = i+1; j < size-2; ++j) {
if(j>i+1 && nums[j] == nums[j-1]) continue; // 3.2避免j位置重复解
int L = j+1, R = size-1;
while(L < R) { // 4.固定i和j,移动双指针L和R
if(nums[i]+nums[j]+nums[L]+nums[R] > target) R--;
else if(nums[i]+nums[j]+nums[L]+nums[R] < target) L++;
else{ //nums[i]+nums[j]+nums[L]+nums[R] == target
res.push_back( {nums[i],nums[j],nums[L],nums[R]} );
while(L<R && nums[L]==nums[L+1]) L++; //4.1避免L位置重复
while(L<R && nums[R]==nums[R-1]) R--; //4.2避免R位置重复
L++; // 4.3将L和R移动到下一位置继续判断
R--;
}
}
}
}
return res;
}
13.删除链表的倒数第N个结点(LeetCode 19)
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
使用快慢指针,使得慢指针恰好指向倒数第n+1个结点。并建立头结点,使得对链表第一个结点的删除操作与其他结点相同。
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyNode = new ListNode(0);//建立头结点,使对第一个结点的删除无需特殊处理
dummyNode->next = head;
ListNode* p = dummyNode;
ListNode* q = dummyNode;
while(n-- > 0) q = q->next; // 指针q先走n步
while(q->next != nullptr){ // 当q指向最后一个结点时,p正好指向倒数第n+1个结点
p = p->next;
q = q->next;
}
ListNode* delNode = p->next;
p->next = delNode->next;
delete delNode;
ListNode* retNode = dummyNode->next; // 链表第一个结点可能已被删除,不能直接返回head指针
delete dummyNode;
return retNode;
}
14.有效的括号(LeetCode 20)
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
输入:s = "()[]{}"
输出:true
栈的应用之括号匹配,使用unordered_map来精简代码,避免每种括号都if一遍。
bool isValid(string s) {
unordered_map<char,int> ump{ {'(',1}, {'[',2}, {'{',3}, {')',4}, {']',5}, {'}',6} };
stack<char> stk;
for(char i : s){
if(1 <= ump[i] && ump[i] <= 3) stk.push(i); // 1.与左括号匹配
else if(!stk.empty() && ump[stk.top()] == ump[i]-3) stk.pop();// 2.当栈stk不为空时,若有输入的右括号与栈顶左括号匹配
else return false; // 3.输入不是括号 或 输入的右括号与栈顶元素不匹配
}
if(!stk.empty()) return false;//若匹配完成后栈不为空(有多余左括号),则匹配失败
return true;
}
15.合并两个有序链表(LeetCode 21)
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
注意对链表的操作都可先建立头结点,使对第一个结点的插入和删除操作与其他结点相同。
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* head = new ListNode(-1);//建立头结点,使第一个结点的插入与其他结点相同(即不用考虑新链表的第一个结点到底是l1还是l2的)
ListNode* p = head; // 移动指针
while(l1!=nullptr && l2!=nullptr) {
if(l1->val <= l2->val) {
p->next = l1;
l1 = l1->next;
}
else {
p->next = l2;
l2 = l2->next;
}
p = p->next;
}
if(l1!=nullptr) p->next = l1;
if(l2!=nullptr) p->next = l2;
ListNode* retNode = head->next;
delete head;
return retNode;
}
16.括号生成(LeetCode 22)
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
全排列问题是在一棵隐式的树上求解,可以用深度优先遍历。由于字符串的特殊性,产生一次拼接都生成新的对象,因此无需回溯。
画图以后,可以分析出的结论:
- 产生左分支的时候,只看当前是否还有左括号可以使用;
- 产生右分支的时候,还受到左分支的限制,右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量的时候,才可以产生分支;
- 在左边和右边剩余的括号数都等于 0 的时候结算。
vector<string> generateParenthesis(int n) {
vector<string> res;
if(n == 0) return res;
dfs("", n, n, res);
return res;
}
/**
* @param curStr 当前递归得到的结果
* @param left 左括号还有几个可以使用
* @param right 右括号还有几个可以使用
* @param res 结果集
*/
void dfs(string curStr, int left, int right, vector<string>& res) {
if(left == 0 && right == 0) { // 1.在左边和右边剩余的括号数都等于 0 的时候结算一次
res.push_back(curStr);
return;
}
if(left > right) return; // 2.剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节)
if(left > 0) { // 3.产生左分支的条件:左括号剩余数量>0
dfs(curStr+"(", left-1, right, res);
}
if(left < right) { // 4.产生右分支的条件:左括号剩余数量<右括号剩余数量
dfs(curStr+")", left, right-1, res);
}
}
17.两两交换链表中的节点(LeetCode 24)
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
输入:head = [1,2,3,4]
输出:[2,1,4,3]
输入:head = [1,2,3]
输出:[2,1,3]
ListNode* swapPairs(ListNode* head) {
ListNode* root = new ListNode();
root->next = head; // 建立头结点,方便对第一个数据处理。
ListNode* pre = root;
while(pre->next && pre->next->next) { // 交换pre->next和pre->next->next两个结点
ListNode* start = pre->next;
ListNode* end = pre->next->next;
// 三步实现结点交换
start->next = end->next; // ①
pre->next = end; // ②
end->next = start; // ③
pre = start; //更新pre位置
}
return root->next;
}
18.删除有序数组中的重复项(LeetCode 26)
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
考虑用 双指针,一个在前初始 p=0,一个在后初始 q=1,算法流程如下:
比较 p 和 q 位置的元素是否相等。
- 如果相等,q 后移 1 位
- 如果不相等,令nums[p+1]=nums[q],p 后移一位,q 后移 1 位
重复上述过程,直到 q 遍历完整个数组。
返回 p + 1,即为新数组长度。
时间复杂度:O(n)。空间复杂度:O(1)。
改进:数组中没有重复元素时,按照上面的方法,每次比较时 nums[p] 都不等于 nums[q],因此就会将 q 指向的元素原地复制一遍,这个操作其实是不必要的。
因此我们可以添加一个小判断,当 q - p > 1 时,才进行复制。
int removeDuplicates(vector<int>& nums) {
if(nums.size() == 0) return 0;
int p = 0, q = 1;
while(q < nums.size()) {
if(nums[p] == nums[q]) { // 前后指针相等
q++;
}
else { // 前后指针不等
if(q-p>1) { // 剪枝,q-p==1时是无意义复制
nums[p+1] = nums[q];
}
p++;
q++;
}
}
return p+1; // 数组长度为数组最大值的下标+1
}
19.下一个排列(LeetCode 31)
实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须 原地 修改,只允许使用额外常数空间。
输入:nums = [1,2,3]
输出:[1,3,2]
输入:nums = [3,2,1]
输出:[1,2,3]
分析:
我们希望下一个数比当前数大,这样才满足“下一个排列”的定义。因此只需要将后面的「大数」与前面的「小数」交换。
我们还希望下一个数增加的幅度尽可能的小,需要在尽可能靠右的低位进行交换,需要从后向前查找,将一个 尽可能小的「大数」 与前面的「小数」交换。
- 从后向前查找第一个相邻升序的元素对(i,j),满足A[i]<A[j]。此时[j,end)必然是降序
- 在[j,end)从后向前查找第一个满足A[i]<A[k]的k。A[i]、A[k]分别就是上文所说的「小数」、「大数」
- 将A[i]与A[k]交换
- 可以断定这时[j,end)必然是降序,逆置[j,end),使其升序
如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤4
时间复杂度O(n),空间复杂度O(1)
void swap(int& a, int& b) {
int temp = a;
a = b;
b =temp;
}
void nextPermutation(vector<int>& nums) {
// next_permutation(nums.begin(),nums.end()); // STL功能和题目描述一致
int n = nums.size();
int i = n-2, j = n-1, k = n-1;
while(i >= 0 && nums[i]>=nums[j]) { // 1.直到找到升序对(i,j),使得A[i]<A[j]才跳出循环。i是较小数。
i--;
j--;
}
if(i >= 0) {
while(nums[k]<=nums[i]) { // 2.直到在[j,end)中逆序找到k,使A[i]<A[k]才跳出循环。k是较大数。
k--;
}
// 3.交换A[i]和A[k]
swap(nums[i], nums[k]);
}
// 4.[j,end)此时必然降序,逆置[j,end),使其升序。
for(int L = j, R = n-1; L < R; L++, R--) {
swap(nums[L], nums[R]);
}
}
20.搜索旋转排序数组(LeetCode 33)
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
即输入的数组是可分成两段,两段分别递增有序。可考虑对经典的二分查找进行改进。
先回顾下对有序数组进行二分查找/折半查找的经典代码。
int search(vector<int>& nums, int target) {
// 对有序数组的二分查找经典代码
int low = 0, high = nums.size()-1, mid = 0;
while(low <= high) {
mid = (low + high) / 2;
if(nums[mid] == target) {
return mid;
}
else if(nums[mid] > target) {
high = mid - 1;
}
else {
low = mid + 1;
}
}
return -1;
}
先根据 nums[mid] 与 nums[low] 的关系判断 mid 是在左段还是右段,接下来再判断 target 是在 mid 的左边还是右边,从而来调整左右边界 low 和 high。
int search(vector<int>& nums, int target) {
int low = 0, high = nums.size()-1, mid = 0;
while(low <= high) {
mid = (low + high) / 2;
if(nums[mid] == target) {
return mid;
}
// 1.先根据 nums[mid] 与 nums[low] 的关系判断 mid 是在左段还是右段
if(nums[mid] >= nums[low]) { // 若mid在左段
// 2.再判断 target 是在 mid 的左边还是右边,从而调整左右边界 low 和 high
if(target >= nums[low] && target <nums[mid]) { // 若target在有序递增的左段
high = mid - 1;
}
else { // 若target在无序的右段,则调整左界
low = mid + 1;
}
}
else { // 若mid在右段
if(target > nums[mid] && target <= nums[high]) { // 若target在有序递增的右段,则调整左界low开始二分查找
low = mid + 1;
}
else { // 若target在无序的左段,则调整右界high
high = mid - 1;
}
}
}
return -1;
}
番外. 二叉树的右视图(LeetCode 199)
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
思路:使用二叉树的层序遍历(即BFS),将层序遍历中每层的最后一个结果加入结果数组。
vector<int> rightSideView(TreeNode* root) {
vector<int> ans;
if (!root) { return ans; } // 若根节点为空
// bfs层序遍历将每层最后一个加入结果数组(树的层序遍历即广度优先遍历)
queue<TreeNode*> que;
que.push(root);
while (!que.empty()) { // 当队列不为空
int size = que.size();
for (int i = 0; i < size; ++i) {
auto node = que.front();
que.pop();
if (node->left) que.push(node->left); // 左孩子入队
if (node->right) que.push(node->right); // 右孩子入队
if (i == size - 1) ans.push_back(node->val); // 每层的最后一个结点值加入数组
}
}
return ans;
}