【LeetCode高频100题-1】冲冲冲

目录

  • 1. 两数之和
    • 题意
    • 解答1:暴力枚举
    • 解答2:哈希
  • 2. 两数相加
    • 题意
    • 解答1:普通解法(类似于合并两个数组)
    • 解答2:对齐补零
    • 解答3:递归
  • 3. 无重复字符的最长子串
    • 题意
    • 解答1 暴力穷举
    • 解答2 滑动窗口+哈希表
  • 4. 寻找两个正序数组的中位数
    • 没理解透彻,暂时搁置,国庆再看 2022.9.24
    • a了但是分类讨论很混乱,下周再看!!! - 2022.10.5
  • 5. 最长回文子串
    • 题意
    • 解答1 中心扩展
    • 解答2 动态规划
  • 10. 正则表达式匹配
    • 题意
    • 解答
  • ==**有错 别看**==
  • 11. 盛最多水的容器
    • 题意
    • 解答 双指针法
  • 15. 三数之和
    • 题意
    • 解答1 双指针法
    • 解答2 哈希表法
  • 17. 电话号码的字母组合
    • 题意
    • 解法1 递归(我的难看的代码)
    • 解法2 回溯(本质是一样的,但是人家写的比我好看多了)
  • 19. 删除链表的倒数第 N 个结点
    • 题意
    • 解法1 计算链表长度(这个方法可以用栈优化)
    • ==解法2 快慢指针==
  • 20. 有效的括号
    • 题意
    • 解法1 栈
  • 21. 合并两个有序链表
    • 题意
    • 解法1 新建结果链表
    • 解法2 拼接结点
    • 解法3 直接插入合并

1. 两数之和

题意

  • 函数要求返回vector类型的结果。
  • 答案唯一。
  • 同一元素不能使用两次。
  • 题目没有说不会出现重复的数字。

解答1:暴力枚举

循环两次,对于每一个数nums[i],寻找其后有没有target-nums[i]的存在。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int tmp=0;
        vector<int> ans;
        for(int i=0;i<nums.size()-1;i++)
        {
            for(int j=i+1;j<nums.size();j++)
            {
                tmp=nums[i]+nums[j];
                if(tmp==target)
                {
                    ans.push_back(i);
                    ans.push_back(j);    
                }
            }
        }
        return ans;
    }
};
// 官方解答
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int n = nums.size();
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                if (nums[i] + nums[j] == target) {
                    return {i, j};
                }
            }
        }
        return {};
    }
};

ATTENTION:

  • return {}是C++11的新特性,他返回的是该函数创建的一个初始对象,在这里这个初始对象的类型就是vector(即返回值类型),而语句return {i, j};就是返回含有元素ij的一个vector
  • 由于编译必须返回值,所以在官方解答中存在语句return {};

解答2:哈希

为了降低复杂度,考虑优化寻找target-nums[i]的过程。这里使用哈希表,对于每一个数nums[i],在哈希表中查找target-nums[i]的存在,如不存在,则将nums[i]插入到哈希表中。

由于哈希表中存的是该数的下标,所以查询的复杂度为O(1),因此这种做法的复杂度为O(n)

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        map<int,int> mp;
        mp[nums[0]]=0;
       for(int i=1;i<nums.size();i++)
       {
           int tmp=target-nums[i];
           map<int,int>::iterator it=mp.find(tmp);
           if(it!=mp.end())
               return {it->second,i};
            mp[nums[i]]=i;
       }
       return {};
    }
};
//官网解答
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hashtable;
        for (int i = 0; i < nums.size(); ++i) {
            auto it = hashtable.find(target - nums[i]);
            if (it != hashtable.end()) {
                return {it->second, i};
            }
            hashtable[nums[i]] = i;
        }
        return {};
    }
};

ATTENTION:

  • map的常用法

2. 两数相加

题意

  • 两个逆序链表,模拟多位数加法。
  • 需要一个变量记录进位。
  • 注意在遍历完两个链表后,需要根据进位进位是否为1,创建最后一个结点。
  • 主要是链表的应用。

解答1:普通解法(类似于合并两个数组)

由于最后需要返回结果链表,所以在创建结果链表l3后,再创建一个指针s指向l3,通过指针s进行链表的增长,那么l3就一直指向结果链表的头结点。

对于链表l1l2,先一起向后遍历,同时记录进位cnt,直到一者为空,只遍历另一者即可。
由于创建结果链表l3时,第一个结点为空节点,所以返回的是l3->next

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* l3=new ListNode();
        ListNode* p=l1,*q=l2,*s=l3;
        int cnt=0;  //进位
        while(p!=NULL&&q!=NULL) 	//注意这里不是->next
        {
            int tmp=p->val+q->val+cnt;
            cnt=0;
            if(tmp>=10)
            {
                cnt=1;
                tmp-=10;
            }
            p=p->next;
            q=q->next; 
            //创建新节点
            ListNode* n=new ListNode(tmp);
            s->next=n;
            s=s->next;    
        }
        while(p!=NULL)
        {
            int tmp=p->val+cnt;
            cnt=0;
            if(tmp>=10)
            {
                cnt=1;
                tmp-=10;
            }
            ListNode* n=new ListNode(tmp);
            s->next=n;
            s=s->next;
            p=p->next;
        }
        while(q!=NULL)
        {
            int tmp=q->val+cnt;
            cnt=0;
            if(tmp>=10)
            {
                cnt=1;
                tmp-=10;
            }
            ListNode* n=new ListNode(tmp);
            s->next=n;
            s=s->next;
            q=q->next;  
        }
        if(cnt==1)
        {
            ListNode* n=new ListNode(cnt);
            s->next=n;
        }
        return l3->next;
    }
};

解答2:对齐补零

通过补零使两个链表长度相同,再进行加法计算。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* p=l1,*q=l2,*l3=new ListNode();
        ListNode* s=l3;
        while(p->next!=NULL||q->next!=NULL) 	//注意这里是next
        {
            if(p->next==NULL)
                p->next=new ListNode(0);
            if(q->next==NULL)
                q->next=new ListNode(0);
            p=p->next;
            q=q->next;
        }
        p=l1;q=l2;
        int cnt=0;
        while(p!=NULL)
        {
            int tmp=p->val+q->val+cnt;
            cnt=0;
            if(tmp>=10)
            {
                tmp-=10;
                cnt=1;
            }
            p=p->next;
            q=q->next;
            s->next=new ListNode(tmp);
            s=s->next;
        }
        if(cnt==1) 	//注意最后的进位!!!
            s->next=new ListNode(1);
        return l3->next;
    }
};

//写法2
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* head=NULL,*tail=NULL;
        int cnt=0; 	//进位应该初始化在循环外面
        while(l1||l2)
        {
        	//这里实际已经实现了补零操作
            int n1=l1?l1->val:0;
            int n2=l2?l2->val:0;
            int tmp=n1+n2+cnt;
            cnt=tmp/10;     //进位
            if(!head)   //头结点未创建
            {
                head=new ListNode(tmp%10);
                tail=head;  //tail遍历结果链表
            }
            else
            {
                tail->next=new ListNode(tmp%10);
                tail=tail->next;
            }
            if(l1) l1=l1->next;
            if(l2) l2=l2->next;
        }
        if(cnt==1) 	//注意最后的进位!!!
            tail->next=new ListNode(1);
        return head;
    }
};
//官方解答
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *head = nullptr, *tail = nullptr;
        int carry = 0;
        while (l1 || l2) {
            int n1 = l1 ? l1->val: 0; 	//l1为真返回l1->val,否则0
            int n2 = l2 ? l2->val: 0;
            int sum = n1 + n2 + carry;
            if (!head) {
                head = tail = new ListNode(sum % 10); 	//这个求余就很妙
            } else {
                tail->next = new ListNode(sum % 10);
                tail = tail->next;
            }
            carry = sum / 10;
            if (l1) {
                l1 = l1->next;
            }
            if (l2) {
                l2 = l2->next;
            }
        }
        if (carry > 0) {
            tail->next = new ListNode(carry);
        }
        return head;
    }
};

解答3:递归

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        int cnt=0;
        return add(l1,l2,cnt);
    }
    ListNode* add(ListNode* l1,ListNode* l2,int cnt)
    {
        if(l1==NULL&&l2==NULL&&cnt==0) return NULL;
        int tmp=0;
        if(l1)
        {
            tmp+=l1->val;
            l1=l1->next; 	//必须在l1有意义时才能next
        } 
        if(l2)
        {
            tmp+=l2->val;
            l2=l2->next; 	//必须在l2有意义时才能next
        } 
        tmp+=cnt;
        cnt=tmp/10;
        ListNode* ans=new ListNode(tmp%10);
        ans->next=add(l1,l2,cnt);   //不能在这里next,因为到了NULL后就没有next了
        return ans;
    }
};

ATTENTION

  • 全局数据区、代码区、堆、栈的区别:
    • 全局数据区:存放静态数据、全局变量和常量。
    • 代码区:存放所有类成员函数和非成员函数的代码。
    • 堆:内存。程序员主动申请newdelete)。动态分配,系统收到请求后,会在堆中保存一个指针,这个指针指向真实分配的内存空间。堆是不连续的空间。向上
    • 栈:存放函数的返回地址、形参、局部变量、返回类型。静态分配,只要栈的剩余空间大于所申请的空间,系统就会自动分配。栈是连续内存。向下
  • 关键字new:在对上创建数组或对象,返回指向这个数组或对象的指针

3. 无重复字符的最长子串

题意

  • 字符串仅包括字母、数字、空格。
  • 数据量为104,可以直接暴力解。

解答1 暴力穷举

最长的不重复子串,最粗暴的想法,就是遍历每个字符,以其为起点,寻找最长的不重复子串。这里我的标记是通过map设置的,费空间而且重置也很费时间,官方解答使用的哈希表十分值得学习。

class Solution {
public:
    map<char,int> mp;
    void mpClear() 	//清空map,以新一轮的标记
    {
        for(int i=0;i<26;i++)
        {
            mp[char('a'+i)]=0;
            mp[char('A'+i)]=0;
        }
        for(int i=0;i<10;i++)
            mp[char('0'+i)]=0;
        mp[' ']=0;
    }
    int lengthOfLongestSubstring(string s) {
        int ans=0;
        for(int i=0;i<s.size();i++)
        {
        	//每一轮遍历,重置子串长度和map
            int tmp=0;
            mpClear();
            for(int j=i;j<s.size();j++)
            {
                if(mp[s[j]]==0)
                {
                    tmp++;
                    mp[s[j]]=1;
                }
                else break; 	//只要遇到出现过的字符,就结束此次遍历
                if(tmp>ans) ans=tmp; 	//并更新ans 这个更新可以用max函数
            }
        }
        return ans;
    }
};

解答2 滑动窗口+哈希表

滑动窗口

我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着「枚举子串的起始位置」。
在每一步的操作中,我们会将左指针向右移动一格,表示我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。
在移动结束后,这个子串就对应着以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;
在枚举结束后,我们找到的最长的子串的长度即为答案。
(官方解释)

判断重复字符

在上面的流程中,我们还需要使用一种数据结构来判断是否有重复的字符,常用的数据结构为哈希集合(即 C++ 中的std::unordered_set,Java 中的 HashSet,Python 中的 set, JavaScript 中的Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。
(官方解释)

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int ans=0;
        int l=0,r=-1;   //初始化:左指针指向第一个元素,右指针应当在左指针左边
        unordered_set<char> st;
        for(;l<s.size();l++)     //枚举左指针(左指针公要移动n-1次)
        {
            if(l!=0)    //左指针移动,需要从哈希表中删去对应元素
                st.erase(s[l-1]);
            while((r+1)<s.size()&&st.count(s[r+1])==0)
            {
                r++;
                st.insert(s[r]);
            }
            ans=max(ans,r-l+1);
        }
        return ans;
    }
};

ATTENTION:
map与unordered_map、set与unordered_set的区别


4. 寻找两个正序数组的中位数

没理解透彻,暂时搁置,国庆再看 2022.9.24

a了但是分类讨论很混乱,下周再看!!! - 2022.10.5


5. 最长回文子串

题意

  • 求取最长回文子串

解答1 中心扩展

对于长度小于等于2的子串s[i,j],若长度为1,即i==j,则为回文子串,若长度为2,即j=i+1,则若s[i]==s[j],则s[i,j]为回文子串。

s[i,j]为回文子串,且s[i-1]==s[j+1],则s[i-1.j+1]也为回文子串,因此可以用中心扩展的方法寻找最长回文子串。

class Solution {
public:
    string longestPalindrome(string s) {
        int n=s.size(),ans=0,ansC=0,ansCnt=0;
        bool flag=false;
        for(int i=0;i<n;i++)
        {
            int cnt=1;
            //奇数情况
            while((i-cnt)>=0&&(i+cnt)<n&&s[i-cnt]==s[i+cnt])
                cnt++;
            if(ans<(2*cnt-1))
            {
                ans=2*cnt-1;
                ansC=i;
                ansCnt=cnt;
                flag=false;
            }
            //偶数情况
            cnt=0;
            while((i-cnt-1)>=0&&(i+cnt)<n&&s[i-cnt-1]==s[i+cnt])
                cnt++;
            if(ans<2*cnt)
            {
                ans=2*cnt;
                ansC=i;
                ansCnt=cnt;
                flag=true;
            }
        }
        if(flag)
            return s.substr(ansC-ansCnt,ansCnt*2);  //substr(起始idx,长度)
        else
            return s.substr(ansC-ansCnt+1,ansCnt*2-1);     //substr(起始idx,长度)
    }
};

解答2 动态规划

对于回文串s[i,j],如果它是长度大于2(这个信息很关键!!!)的回文串,那么s[i+1,j-1]也定是一个回文串,因此,可以尝试使用动态规划解决此题。

**只有先讨论了长度小于等于2的回文串,才能讨论长度大于2的回文串。**对于长度大于2的子串s[i,j],如果s[i+1,j-1]是回文子串,并且s[i]==s[j],则s[i,j]也是回文子串。

必须倒着遍历!!!如下图:
例如判断子串s[0,4]是否是回文串,则需先判断s[1,3]是否是回文串,要判断s[1,3]是否是回文串,则需先判断s[2,2]是否为回文串,因此i要从后往前遍历,j从前往后遍历 (为什么j从后往前遍历也能ac?)
【LeetCode高频100题-1】冲冲冲_第1张图片

class Solution {
public:
    string longestPalindrome(string s) {
        int n=s.size();
        int ansCnt=1,ansIdx=0;
        int dp[n][n];   //怎么初始化
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                dp[i][j]=0;
        for(int i=0;i<n;i++)
        {
            dp[i][i]=1;
            if(i!=n-1&&s[i]==s[i+1])
            {
                dp[i][i+1]=1;
                if(ansCnt<2)
                {
                    ansCnt=2;
                    ansIdx=i;
                }
            }
                
        }
        for(int i=n-1;i>=0;i--)
        {
            for(int j=i;j<n;j++)
            {
                if(j-i+1>2)
                {
                    if(s[i]==s[j]&&dp[i+1][j-1]==1)
                    {
                        dp[i][j]=1;
                        if(ansCnt<j-i+1)
                        {
                            ansCnt=j-i+1;
                            ansIdx=i;
                        }
                    }
                }
            }
        }
        return s.substr(ansIdx,ansCnt);
    }
};

10. 正则表达式匹配

题意

  • ·:可以匹配任意一个字符。
  • *:可以匹配0次或多次的前一个字符,也就是说,*可以让前面那个字符消失。
    e.g. ·*:可以匹配0个字符或者多个·

解答

为了处理两个空字符串相匹配的情况,将dp矩阵设为dp[s.size()+1][p.size()+1],其中,dp[i][j]表示s的前i个字符与p的前j个字符的匹配情况,dp[0][0]表示了两个空字符串相匹配的情况。(注意字符串的下标从0开始,记得对应)

p[j]为字符或者'·'时,都只需比较s[i]p[j]即可;但是,如果p[j]'*',这时候就要将p[j-1]p[j]作为一个整体一起考虑。

有错 别看

【LeetCode高频100题-1】冲冲冲_第2张图片
对于p[j]'*'的情况,要么,将p[j-1]p[j]一起删去,要么,从s中删去一个字符s[i-1]

class Solution {
public:
    bool isEqual(int i,int j,string s,string p)
    {
        //下标到字符串下标的对应,要-1!!!
        //中文字符int值太大,无法直接比较 阿西吧,这是个句号
        // if(j=='·') return true;
        // return i==j;
        if(i==0) return false;  //
        if(p[j-1]=='.') return true;
        return s[i-1]==p[j-1];
    }
    bool isMatch(string s, string p) {
        int m=s.size(),n=p.size();
        vector<vector<int> > dp(m+1,vector<int>(n+1));  //二维数组初始化
        dp[0][0]=1;     //空字符串的匹配
        for(int i=0;i<m+1;i++)
        {
            for(int j=1;j<n+1;j++)
            {
                if(p[j-1]=='*')
                {
                    dp[i][j]=dp[i][j-2]||dp[i][j];  //tmd 官方解答里是异或,所以额外的状态转移不会对结果产生影响!!!
                    if(isEqual(i,j-1,s,p)) dp[i][j]=dp[i-1][j]||dp[i][j];
                }
                else
                {
                    if(isEqual(i,j,s,p))
                        dp[i][j]=dp[i-1][j-1]||dp[i][j];
                }
            }
        }
        return dp[m][n];
    }
};

ATTENTION:

  • auto:当编译器能够在一个变量的声明时候就推断出它的类型,那么你就能够用auto关键字来作为他们的类型。
  • 匿名函数 lambda的声明与调用:
[captures] (params) {Statments;} 	//[捕获列表](形参){函数体}

capture:
[] 不截取任何变量
[&] 截取外部作用域中所有变量,并作为引用在函数体中使用

  • 匿名函数,举个例子:
//可以通过auto获取返回值
auto ret = [](auto a, auto b) {return a + b; }(1, 1.5); 
cout << "ret = " << ret << endl;

11. 盛最多水的容器

题意

  • 就是寻找最大的矩形。
  • 双指针法。

解答 双指针法

首先考虑暴力穷举,复杂度为O(n2),由于数据的数量级为n5,所以复杂度太高,不能直接暴力穷举。所以要删减穷举的情况,这就用到了双指针法。

考虑两板[i,j],其宽度为width=j-i,而其高度由更短的板决定,即高度为min(height[i],height[j]),所以水的面积为(j-i)*min(height[i],height[j])。现在考虑移动板。

假设height[i],此时,不论是向内移动短板,还是向内移动长板,宽度都是减少的,但是高度不同,需要分类讨论。

  • 如果将长板j向内移动,那么得到的新板j'有两种情况:①比原来的短板低;②比原来的短板高。如果j'高于短板i,水的高度不变;j'如果低于短板i,那么水的高度还会减少。所以将长板内移,最后得到的水的面积一定不会增加,因此不需要考虑这些情况。
  • 如果将短板i向内移动,那么得到的新板i'如果高于旧短板i,水的高度就会升高,虽然宽度减少了,但是水的面积不一定会减少,因此此时可能获得更大的水的面积。

所以,最后得到的结论就是:一直向内移动短板。这可以大大地减少面积不会增加的情况,同时考虑所有面积可能增加的情况。

双指针法的复杂度为O(n),即双指针一共最多遍历整个数组一遍

class Solution {
public:
    int maxArea(vector<int>& height) {
        int ans=0,n=height.size();
        int left=0,right=n-1;
        while(left<right)
        {
            int w=right-left,h=min(height[left],height[right]);
            if(ans<w*h)
                ans=w*h;
            if(height[left]<height[right])
                left++;
            else
                right--;
        }
        return ans;
    }
};

15. 三数之和

题意

  • 数组nums[]是可能会出现重复数字的。
  • 寻找所有相加为0的三元组,但是不能重复
  • 若不给数组nums[]排序,对于不同位置的但是重复的元素,可能会搜索到重复的三元组,如nums[]={-1,-1,1,0,-1}
  • 数据规模为103,也就是说复杂度要小于O(N3)

解答1 双指针法

首先对于寻找一个和为0的三元组,可以等价于寻找一个和为-a的二元组,从而可以将三数之和的问题转换为两数之和。两数之和可以用哈希表或者双指针(元素有序的前提下) 求解。

此题的难点更多是在于要求返回的三元组不重复。由于返回的是vector >类型的对象,所以很难在最后对其搜索去重,因此要在求解三元组的同时,就删去那些重复的三元组

首先对数组nums[]进行排序,方便后面操作中不处理重复的数字。然后从左往右遍历数组nums[],对于每一个对象nums[i]只在其后寻找可能产生的二元组(满足b+c=-a),从而避免一部分的重复三元组。

接着遍历nums[i]之后的对象,记为nums[j]

对于双指针法,使left指向jright指向n-1,寻找二元组(满足b+c=-a)。由于数组nums[]是有序的,所以在寻找时可以非常方便地通过比较b+c-a的大小,进行左右指针的移动。 在寻找到一个答案后,左右指针都要向内移动一格并且新指向的数字不能与原数字相同,否则会导致重复的答案。

双指针法的复杂度为O(n2)

vector<vector<int>> threeSum(vector<int>& nums) {
    int n=nums.size(),cnt=0;
    vector<vector<int> > ans;

    //先给vector排序
    sort(nums.begin(),nums.end());
    
    //对于每个nums[i],在其之后寻找两个数相加为target=0-nums[i]
    for(int i=0;i<n;i++)
    {
        if(i!=0&&nums[i]==nums[i-1])    //不处理重复的元素
            continue;
        int target=0-nums[i];
        //从nums[i]之后中寻找两个数和为target
        //双指针法
        int left=i+1,right=n-1;
        bool flag=false;
        while(left<right)   //要有两个数,所以不能取等
        {
            if(flag)
            {
            	//已找到一个三元组,不处理重复的元素
                if(nums[left-1]==nums[left])
                {
                    left++;
                    continue;
                }
                if(nums[right+1]==nums[right]) 
                {
                    right--;
                    continue;
                }
                flag=false;
            }
            if((nums[left]+nums[right])<target) left++;
            else if((nums[left]+nums[right])>target) right--;
            else
            {
                //找到一个三元组
                vector<int> tmp={nums[i],nums[left],nums[right]};
                ans.push_back(tmp); 	//插入
                flag=true;
                //左右指针都内移一位,继续寻找新的答案
                left++;
                right--;
            }
        }
    }
    return ans;
} 

解答2 哈希表法

再看哈希表处理两数之和问题:

因为数组nums[]是有序的,所以不妨假设二元组(b<=c)。显然,当nums[j]=b时,哈希表中不存在可以匹配的c,只有当nums[j]=c时,才能在哈希表中找到可以匹配的b

对于nums[j]=b,如果c不在哈希表中,那么就将num[j]插入到哈希表中;如果c在哈希表中,那么就找到了一个解答,将该三元组保存,并且还要将nums[j]插入到哈希表中(因为这个c可能是下一个b)。

需要注意的是,b、c可能不唯一

  • b时,
    • 如果存在多个b,由于哈希表是通过set实现的,所以哈希表中不会存在两个b,所以不唯一的b不会影响搜寻结果。
    • 但是不唯一的c不同,比如nums[]={-1,0,1,1},如若不做约束,当nums[j]为第二个1时,就会搜寻到一个重复的三元组<-1,0,1>(因为此时哈希表中存在-1和0)。所以在搜寻到一个符合的三元组时,需要对接下来遍历的nums[j]进行判断,保证下一个nums[j]不能为上一个c
  • b=c时,相当于bc都不唯一,与b时处理雷同。

PS.如果在搜索到一个二元组后,将bc都从哈希表中删去是否能避免重复的情况呢?
不能! 例如nums[]={0,0,0,0,0},当nums[j]为第二个0时,搜索到一个符合的二元组<0,0>,此时将0从哈希表中删去,但是当nums[j]为第四个0时,哈希表中又存在了一个可以匹配的0,此时,会出现重复的答案。

按理说,哈希表法的复杂度也为O(n2),但是在实际运行中,速度远比双指针法慢,也更加耗空间。

	vector<vector<int>> threeSum(vector<int>& nums) {
    int n=nums.size(),cnt=0;
    vector<vector<int> > ans;

    //先给vector排序
    sort(nums.begin(),nums.end());
    
    //对于每个nums[i],在其之后寻找两个数相加为target=0-nums[i]
    for(int i=0;i<n;i++)
    {
        if(i!=0&&nums[i]==nums[i-1])    //不处理重复的元素
            continue;
        int target=0-nums[i];
        //从nums[i]之后中寻找两个数和为target
        unordered_set<int> st;  //哈希表中只会存在nums[i]以后的元素
        bool flag=false;
        for(int j=i+1;j<n;j++)
        {
            if(flag)    //当搜索到一个答案时,要对下一个nums[j]进行约束,不能是重复的数字
            {
                if(nums[j]==nums[j-1]) continue;
                flag=false;     //直到nums[j]!=nums[j-1],重置flag,开始搜索
            }
            if(st.find(target-nums[j])==st.end()) 
                st.insert(nums[j]);     //没找到,插入到哈希表中
            else
            {
                // cout<
                vector<int> tmp={nums[i],nums[j],target-nums[j]};   //这个插入应该怎么写
                ans.push_back(tmp);

                st.erase(target-nums[j]);
                if(target-nums[j]!=nums[j])
                    st.insert(nums[j]);     //找到也要插入到哈希表中,因为可能还有别的答案
                //st.insert(nums[j]); 上三行改为这一行 不影响结果,也就是说可以不删除哈希表中的b
                flag=true;
            }        
        }
    }
    return ans;
}    

ATTENTION

  • sort函数头文件:#include
  • vector > vec赋值:
vector<int> tmp={...};
vec.push_back(tmp);
//不能直接给vec[][]赋值。

17. 电话号码的字母组合

题意

  • 如果已知digits的长度,那么可以直接暴力嵌套for循环,但这道题的难点就在于,digits的长度不定,所以需要用递归来实现for循环的嵌套。

解法1 递归(我的难看的代码)

首先判断digits的长度,如果digits.size()==0,那么直接返回空;如果digits.size()!=0,开始递归。

个人认为,自己写的递归不够精简,因为我把第一个数字给单独拿出来了,只递归了后面n-1个数字。

对于第一个数字对应的每一个字母tmp[i],调用递归函数nextNumber(现在的字符串,下一个数字的下标,digits)

在递归函数nextNumber(string s,int n,string digits)中,如果此时n==digits.size(),说明此时没有下一个数字需要排列组合了,此时将得到的字符串s插入到ans数组中。否则,对于当前的数字digits[n],对于它对应的每一个字母,都继续调用递归函数nextNumber,继续与下一个数字进行排列组合,而此时传入函数nextNumber的字符串为s+tmp[i]

数字与字母的对应我写的很丑,可以学习别人的简洁写法。

class Solution {
public:
    vector<string> ans;
    //这个match可以优化
    vector<string> matchedVector(char c)
    {
        if(c=='2')
        {
            vector<string> two={"a","b","c"};
            return two;
        }
        else if(c=='3')
        {
            vector<string> three={"d","e","f"};
            return three;
        }
        else if(c=='4')
        {
            vector<string> four={"g","h","i"};
            return four;
        }
        else if(c=='5')
        {
            vector<string> five={"j","k","l"};
            return five;
        }
        else if(c=='6')
        {
            vector<string> six={"m","n","o"};
            return six;
        }
        else if(c=='7')
        {
            vector<string> seven={"p","q","r","s"};
            return seven;
        }
        else if(c=='8')
        {
            vector<string> eight={"t","u","v"};
            return eight;
        }
        else
        {
            vector<string> nine={"w","x","y","z"};
            return nine;
        }
    }
    void nextNumber(string s,int n,string digits)
    {
        if(n==digits.size())
        {
            ans.push_back(s);
            return;
        }
        //遍历每一个字母
        vector<string> tmp=matchedVector(digits[n]);    //对应的字母
        for(int i=0;i<tmp.size();i++)
            nextNumber(s+tmp[i],n+1,digits);    //return就退出函数了,所以要在遍历完所有情况后再return。 
    }
    vector<string> letterCombinations(string digits) {
        if(digits=="") return ans;
        //这里的tmp只有在digits不为空字符串时才有意义,否则会指向非法内存。
        vector<string> tmp=matchedVector(digits[0]);
        string s="";
        for(int i=0;i<tmp.size();i++)
            nextNumber(s+tmp[i],1,digits);
        return ans;
    }
};

解法2 回溯(本质是一样的,但是人家写的比我好看多了)

为什么可以这么简洁…

class Solution {
public:
    void backTrack(vector<string>& ans,const unordered_map<char,string>& matched,const string& digits,int idx,string& s)
{
    if(idx==digits.size())
    {
        ans.push_back(s);
        return ;
    }
    else
    {
        char digit=digits[idx];
        const string& letters=matched.at(digit);
        for(const char& letter:letters)
        {
            s.push_back(letter);    //string竟然也可以push_back(x)!!!
            backTrack(ans,matched,digits,idx+1,s);
            s.pop_back();   //string竟然也可以pop_back()!!!
        }
    }
}
    vector<string> letterCombinations(string digits) {
        vector<string> ans;
        if(digits.size()==0) return ans;
        unordered_map<char,string> matched{
            {'2',"abc"},
            {'3',"def"},
            {'4',"ghi"},
            {'5',"jkl"},
            {'6',"mno"},
            {'7',"pqrs"},
            {'8',"tuv"},
            {'9',"wxyz"}
        };
        string s;
        backTrack(ans,matched,digits,0,s);
        return ans;
        
    }
};

ATTENTION

  • 递归形式
void recursion()
{
    if(/*递归结束条件*/)
    {
        //退出
        return;
    }
    //继续递归
    recursion();
}
  • for循环中的return问题
    一开始在写代码的时候,本来是想让nextNumber函数返回一个string类型的对象,表示生成的字符串,然后在递归外面插入到ans数组中去,但是一直出现无法遍历除了第一个数字以外数字代表的字母的问题,后来发现是for循环中的return的问题。当在for循环中遇到return时,函数会直接中断for循环,不仅如此,子函数会直接结束,退出到调用子函数的函数中
int test()
{
    for(int i=0;i<10;i++)
    {
        cout<<i<<endl;
        for(int j=1;j<10;j++)
        {
            cout<<j<<endl;
            return 2;
        }
    }
}
int main()
{
    int a=test();
    cout<<"a"<<a<<endl;
}
//输出值:
0
1
a2
  • string也可以push_back(x)pop_back()!!!
  • C++ for循环新用法(本质是借助迭代器)
for(auto c:arr)
{...}

19. 删除链表的倒数第 N 个结点

题意

  • 链表的删除操作的变种。

解法1 计算链表长度(这个方法可以用栈优化)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        if(head->next==NULL) return NULL;
        int size=0;
        ListNode* p=head;
        while(p!=NULL)
        {
            size++;
            p=p->next;
        }
        if(n==size) 	//删除第一个结点
            head=head->next;
        else
        {
            int m=size-n+1;
            ListNode* p=head,*q=head;
            for(int i=0;i<m-1;i++)
            {
                q=p;
                p=p->next;
            }
            q->next=p->next;
        }
        return head;    
    }
};

解法2 快慢指针

设两个指针,一个快指针,一个慢指针,两者相差n个结点,那么当快指针指向NULL的时候,慢指针指向的就是要删除结点的前一个结点。(妙)

官方实现里在在第一个结点前加了一个空结点,从而统一第一个结点和其他结点的处理方式。(链表题的常用技巧:dummpy节点)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* p=head,*q;
        while(n!=0)
        {
            n--;
            p=p->next;  //快指针
        }
        if(p==NULL) return head->next;  //删除第一个结点
        q=head;     //慢指针
        while(p->next!=NULL)
        {
            p=p->next;
            q=q->next;
        }
        q->next=q->next->next;  //不是p->next!!! p和q不一定是相邻关系
        return head;
    }
};

20. 有效的括号

题意

  • 只有()、[]、{}三种括号,进行括号匹配。

解法1 栈

  • 遇到左括号入栈
  • 遇到右括号,查看栈。如果栈空,则匹配失败;如果栈非空,则看栈顶是否是匹配的左括号,若是,则匹配成功,左括号出栈,继续遍历,否则匹配失败。
  • 最后,如果栈非空,说明还有未匹配的左括号,则匹配失败。
class Solution {
public:
    bool isValid(string s) {
        stack<char> st;
        for(int i=0;i<s.size();i++)
        {
            if(s[i]=='('||s[i]=='['||s[i]=='{')
                st.push(s[i]);
            else
            {   //栈空,失败
                if(st.empty()) return false;
                //栈顶是否是匹配的左括号
                if(s[i]==')')
                    if(st.top()=='(') st.pop();
                    else return false;
                else if(s[i]==']')
                    if(st.top()=='[') st.pop();
                    else return false;
                else
                    if(st.top()=='{') st.pop();
                    else return false;
            }
        }
        if(!st.empty()) return false;
        return true;
    }
};

ATTENTION

  • 题目本身不难,但是不能一遍ac,考虑情况十分不全面,注意不能永远通过提交报错来发现没考虑到的情况。

21. 合并两个有序链表

题意

解法1 新建结果链表

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* head=new ListNode();  //初始化 leetcode必须初始化
        ListNode *p=list1,*q=list2;
        ListNode* tail=head;
        while(p!=NULL&&q!=NULL)
        {
            cout<<p->val<<" "<<q->val<<endl;
            while(p!=NULL&&q!=NULL&&p->val<=q->val)     //NULL判断,外围判断此时不奏效(while嵌套while时需要格外注意)
            {
                ListNode* newNode=new ListNode(p->val);     //new创建的是指针
                tail->next=newNode;
                tail=tail->next;
                p=p->next;
            }
            while(p!=NULL&&q!=NULL&&p->val>q->val)  //NULL判断,外围判断此时不奏效(while嵌套while时需要格外注意)
            {
                ListNode* newNode=new ListNode(q->val);
                tail->next=newNode;
                tail=tail->next;
                q=q->next;
            }
        }
        while(q!=NULL)
        {
            ListNode* newNode=new ListNode(q->val);
            tail->next=newNode;
            tail=tail->next;
            q=q->next;
        }
        while(p!=NULL)
        {
            ListNode* newNode=new ListNode(p->val);
            tail->next=newNode;
            tail=tail->next;
            p=p->next;
        }
        return head->next;
    }
};

解法2 拼接结点

//官方代码,有点玄妙。
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
       if(list1==NULL) return list2;
       else if(list2==NULL) return list1;
       else if(list1->val<list2->val)
       {
            list1->next=mergeTwoLists(list1->next,list2);
            return list1;
       }
       else
       {
           l2->next=mergeTwoLists(list1,list2->next);
           return list2;
       }
    }
};

解法3 直接插入合并

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if((!list1)||(!list2)) return list1?list1:list2;
        ListNode head;   //新建空头结点,统一所有节点的处理方式 不能用new初始化
        ListNode*a=list1,*b=list2,*tail=&head;  //指针指向head的地址
        while(a&&b)
        {
            if(a->val<b->val)
            {
                tail->next=a;
                a=a->next;
            }
            else if(b->val<=a->val)     //如果不考虑等于的情况,在a->val和b->val取等时,tail会被赋值为NULL->next,所以会报错。
            {
                tail->next=b;
                b=b->next;
            }
            tail=tail->next;
        }
        tail->next=a?a:b;
        // while(a!=NULL)
        // {
        //     tail->next=a;
        //     a=a->next;
        //     tail=tail->next;
        // }
        // while(b!=NULL)
        // {
        //     tail->next=b;
        //     b=b->next;
        //     tail=tail->next;
        // }
        return head.next;   //指针用->
    }
};

ATTENTION

  • 虚拟头结点 (dummy):链表题常用技巧,可以避免处理空指针的情况,降低代码的复杂性。
  • ListNodeListNode*两种对象类型
    • 注意区分ListNodeListNode*两种对象类型的区别。ListNode是一个节点,实质上是一个有一定结构的结构体,包括了val部分和next部分,它通过.来获取值;ListNode*是一个指针,实质上只占据了一个指针单位的存储空间,它指向一个ListNode对象,它通过->来获取值。
    • 所以,如果要新建一个空头节点,应当使用ListNode类型,而不是ListNode*类型。而如果要新建一个指向链表节点的指针,应当使用ListNode*类型,并且,为了防止指向非法内存,要通过newnew只能用来初始化指针)对其初始化或者初始化为NULL
    • 而如果想要使一个ListNode*类型的对象指向一个ListNode,应当使用&运算符取节点的地址为其赋值(ListNode* tail=&head;)。
  • 代码中,ab两个指针虽然实质上只指向一个链表节点,但是由于链表的特殊性质,ab也相当于指向了一段链表,所以可以直接tail->next=a?a:b;将剩余的链表连接到tail的后面。

你可能感兴趣的:(数据结构,leetcode,leetcode,链表,数据结构)