目录
9、删除链表的倒数第N个节点 - 链表 + 快慢指针 / 栈
1、两数之和 - 哈希表
思路:
建立map,mp[nums[i]]=i 存储值所对应的下标
顺序遍历每一个元素,先查找mp中是否存在与nums[i]匹配的值(target-nums[i])
如果存在,则返回【i,匹配值下标i】
否则将该nums[i]存入map中
为什么先查找,再存哈希表?
因为题目要求,数组中同一元素不能重复出现,所以当遍历到自己时,先查找之前有没有存在的匹配值,再进行存储,这样避免了匹配到自己的情况
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> mp = new HashMap<>();
for(int i=0; i < nums.length; i++ )
{
int t = target - nums[i];
if(mp.containsKey(t)) return new int[] {mp.get(t),i};
mp.put(nums[i],i);
}
return new int[] {};
}
}
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int n=nums.size();
map<int,int> mp;
for(int i=0;i<n;i++)
{
int t=target-nums[i];
if(mp.find(t)!=mp.end())
return {i,mp.find(t)->second};
mp[nums[i]]=i;
}
return{};
}
};
2、两数相加 - 链表
思路:
l1和l2指针顺序向后遍历两链表
如果指到非空节点,存下各位之和sum和进位数t
注意:如果遍历到最后仍有进位,需要再在链表后新增节点
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *head = nullptr,*tail = nullptr;
int t = 0;
while(l1||l2)
{
int x1 = l1? l1->val:0;
int x2 = l2? l2->val:0;
int sum = (x1+x2+t)%10;
t = (x1+x2+t)/10;
if(!head)
head = tail = new ListNode(sum);
else{
tail->next = new ListNode(sum);
tail = tail->next;
}
if(l1) l1 = l1->next;
if(l2) l2 = l2->next;
}
if(t>0) tail->next = new ListNode(t);
return head;
}
};
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy=new ListNode();
ListNode cur=dummy;
int t=0;
while(l1!=null||l2!=null||t>0)
{
if(l1!=null) t+=l1.val;
if(l2!=null) t+=l2.val;
cur=cur.next=new ListNode(t%10);
t=t/10;
if(l1!=null) l1=l1.next;
if(l2!=null) l2=l2.next;
}
return dummy.next;
}
}
3、无重复字符的最长子串 - 滑动窗口
思路:
用set判重,双指针实现滑动窗口
用maxx更新最长长度
如果窗口内出现重复元素,缩左边界,并将不在窗口内的字符删去,直至窗口内无重复元素
即若窗口内无重复元素,向右增大窗口,并更新最大长度,直至出现重复元素,缩小窗口,最后滑窗遍历完整个字符串,取最大窗口长度
class Solution {
public:
int lengthOfLongestSubstring(string s) {
set<char> word;
int st=0,maxx=0;
for(int ed=0;ed<s.size();ed++)
{
while(word.count(s[ed]))
{
word.erase(s[st]);
st++;
if(!word.count(s[ed]))continue;
}
if(ed-st+1>maxx) maxx=ed-st+1;
word.insert(s[ed]);
cout<<ed<<' '<<st<<endl;
}
return maxx;
}
};
4、寻找两个正序数组的中位数 - 暴力合并两数组
思路:
暴力求解,没啥好说的,进阶的不想看
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m=nums1.size(),n=nums2.size();
cout<<m<<" "<<n;
int t=m+n+10;
int arr[t];
int l1=0,l2=0,cnt=0;
while(l1<m&&l2<n)
{
if(nums1[l1]>nums2[l2]) arr[cnt++]=nums2[l2],l2++;
else arr[cnt++]=nums1[l1],l1++;
}
while(l1<m) arr[cnt++]=nums1[l1],l1++;
while(l2<n) arr[cnt++]=nums2[l2],l2++;
if((m+n)%2) return arr[(m+n-1)/2];
else return (arr[(m+n)/2]+arr[(m+n)/2-1])*1.0/2;
}
};
5、最长回文子串 - 中心扩展法 + dp动态规划
常规暴力O(n^3) 会tle
(1)中心扩展法
思路:
枚举每一个字符s[i],同时设置左右指针l=i-1,r=i+1,最长回文子串长度len
如果该子串为回文串,会有如下三种情况:
- 左指针和中心字符相等,len+1,例如:abaacb
- 右指针和中心字符相等,len+1,例如:abcaab
- 左指针和右指针字符相等,len+2,如:abcacb
如果左右指针不越界,循环拓展以上三种情况,并实时更新最长回文子串长度
因为遍历的是以每一个字符为中心扩展的情况,因此遍历完字符串,所有情况的子串情况也都考虑到了,时间复杂度O(n^2)
class Solution {
public:
string longestPalindrome(string s) {
string res="";
int n=s.size();
for(int i=0;i<n;i++)
{
int len=1;
int l=i-1,r=i+1;
while(l>=0 && s[l]==s[i])
len++,l--;
while(r<n && s[r]==s[i])
len++,r++;
while(l>=0 && r<n && s[l]==s[r])
len+=2,l--,r++;
if(len>res.size())
res=s.substr(l+1,len);
}
return res;
}
};
(2)dp
思路:
- dp[i][j]:表示区间范围[i,j]的子串是否是回文子串,如果dp[i][j]为true,否则为false
- 初始化dp[i][i] = true,因为单个字符自己就是回文串
只考虑s[i]和s[j]相等的情况,因为不相等根本就不可能为回文串(初始化dp全为false,所以只要把所有是回文串的情况标为true即可)
dp是以小范围的状态去更新大范围的状态,因此需要用短子串去更新长子串
dp[i][j]的状态取决于dp[i+1][j-1],如果i和j中间就1个字符,那必然是回文串,即就是j-1-(i+1)+1<2,化简即为j - i < 3,也就是说j - i < 3的情况dp[i][j]必为true
其他情况则用dp[i+1][j-1]来更新
最后遍历所有dp情况,如果dp=true,则更新最长回文子串长度
class Solution {
public String longestPalindrome(String s) {
int n=s.length();
if(n<2) return s;
boolean[][] dp=new boolean[n][n];
int maxlen=1,st=0;
for(int i=0;i<n;i++)
dp[i][i]=true;
for(int j=1;j<n;j++)
for(int i=0;i<j;i++)
{
if(s.charAt(i)==s.charAt(j))
{
if(j-i<3) dp[i][j]=true;
else dp[i][j]=dp[i+1][j-1];
}
if(dp[i][j] && j-i+1>maxlen)
{
st=i;
maxlen=j-i+1;
}
}
return s.substring(st,st+maxlen);
}
}
6、三数之和 - 双指针 + 判重
这题难点是:不能出现重复情况
思路:
首先对数组进行升序排序,因为三数之和=0,排序后能更好地去掉不可能为0的方案
遍历数组中每一个数作为第一个数x1
- 如果x1>0,后面的数都比x1大,不可能有=0的方案存在
- 如果遍历到的x1和前一个遍历过的元素值相同,则所生成的方案也是相同的,跳过
- l 和 r 分别指向x1后的最前和最末尾元素,如果指向的三个之和=0则记录方案
- 注意:如果s【l+1】和s【l】值一样,则需要跳过,因为s【l】作为x2的方案之前已经考虑过,而题目要求不能有重复情况,s【r】同理
- (若两个数固定,剩下一个必固定,题目要求不能重复)
- 如果三数之和sum>0,则r--
- 如果sum<0,l++
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
int n=nums.size();
sort(nums.begin(),nums.end());
if(n<3) return res;
for(int i=0;i<n;i++)
{
if(nums[i]>0) continue;
if(i>0&&nums[i]==nums[i-1]) continue;
int l=i+1,r=n-1;
while(l<r){
int sum=nums[i]+nums[l]+nums[r];
if(sum==0)
{
res.push_back({nums[i],nums[l],nums[r]});
while(l<r&&nums[l]==nums[l+1]) l++; //去重
while(l<r&&nums[r]==nums[r-1]) r--;
l++,r--; //若两个数固定,剩下一个必固定,题目要求不能重复,因此移动双指针
}else if(sum>0) r--;
else l++;
}
}
return res;
}
};
7、盛水最多的容器 - 思维 + 双指针
思路:
从两侧枚举板子
因为装水的高度取决于短板
若移动短板,则短板为min(h[i],h[j]),短板有可能变长,水体积可能增加
若移动长板,就算长板更长,也无法改变水的体积,因此水体积减小(间距变小),因此绝对不能移动长板
class Solution {
public:
int maxArea(vector<int>& h) {
// 从两侧枚举板子
// 因为装水的高度取决于短板
// 若移动短板,则短板为min(h[i],h[j]),短板有可能变长,水体积可能增加
// 若移动长板,就算长板更长,也无法改变水的体积,因此水体积减小(间距变小),因此绝对不能移动长板
int i=0,j=h.size()-1;
int res=0;
while(i<j)
{
res= h[i]>h[j]?
max(res,(j-i)*h[j--]):max(res,(j-i)*h[i++]);
}
return res;
}
};
8、电话号码的字母组合 - dfs + 组合
思路:
【leetcode学习计划】算法入门(14 / 14)完结!_leetcode算法学习计划_Roye_ack的博客-CSDN博客
这题就是dfs组合问题的升级版
根据题意,digits长度就是每个组合的长度,我们可以枚举每一个位置的数字
用哈希表存每个数字对应的字母
如果每个位置都枚举完,则将该种情况存入答案
class Solution {
public:
vector<string> tele={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> res;
string t;
void dfs(int pos,string digits)
{
if(pos==digits.size())
{
res.push_back(t);
return;
}
int num=digits[pos]-'0';
for(int i=0;i<tele[num].size();i++)
{
t.push_back(tele[num][i]);
dfs(pos+1,digits);
t.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits.size()==0) return res;
dfs(0,digits);
return res;
}
};
9、删除链表的倒数第N个节点 - 链表 + 快慢指针 / 栈
(1)暴力 计算链表长度
移动到要删除的节点的前一个,然后将该节点指向待删除节点的下一个,实现节点删除
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy=new ListNode(0);
dummy->next=head;
ListNode* cur=head;
int len=0;
while(cur!=nullptr)
{
len++;
cur=cur->next;
}
cur=dummy;
for(int i=0;i<len-n;i++)
{
cur=cur->next;
}
cout<<cur->val<<endl;
cur->next=cur->next->next;
return dummy->next;
}
};
(2)快慢指针
- 定义快指针q和慢指针p,两者初始同时指向虚拟节点dummy
- 先移动快指针q使pq间隔n个元素,随后同时移动pq指针,直至q指向链表末尾null,此时,p指针的下一个节点就是待删除节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy=new ListNode(0);
dummy.next=head;
ListNode p=dummy,q=dummy;
for(int i=0;i<n+1;i++)
q=q.next;
while(q!=null)
{
p=p.next;
q=q.next;
}
p.next=p.next.next;
return dummy.next;
}
}
(3)栈
删除倒数第n个节点,可以利用栈的特性,让所有元素都入栈,然后扔出栈中n个节点,此时栈顶元素就是待删除的节点的前一个,然后再执行删除节点操作
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
stack<ListNode*> stk;
ListNode* cur = dummy;
while (cur) {
stk.push(cur);
cur = cur->next;
}
for (int i = 0; i < n; ++i) {
stk.pop();
}
ListNode* prev = stk.top();
prev->next = prev->next->next;
ListNode* ans = dummy->next;
delete dummy;
return ans;
}
};
10、有效的括号 - 暴力 + 栈 + 哈希表
(1)暴力+栈
class Solution {
public boolean isValid(String s) {
if(s.length()%2==1) return false;
Deque<Character> stk=new LinkedList<>();
for(int i=0;i<s.length();i++)
{
char c=s.charAt(i);
if(c=='('||c=='['||c=='{')
stk.push(c);
else if(c==')')
{
if(stk.isEmpty()||stk.peek()!='(') return false;
stk.pop();
}else if(c==']')
{
if(stk.isEmpty()||stk.peek()!='[') return false;
stk.pop();
}else if(c=='}')
{
if(stk.isEmpty()||stk.peek()!='{') return false;
stk.pop();
}
}
if(!stk.isEmpty()) return false;
return true;
}
}
(2)哈希表+栈
class Solution {
public:
bool isValid(string s) {
int n=s.size();
if(n%2) return false;
stack<char> stk;
unordered_map<char,char> mp={
{')','('},
{']','['},
{'}','{'}
};
for(int i=0;i<n;i++)
{
if(mp.count(s[i]))
if(stk.empty()||mp[s[i]]!=stk.top())
return false;
else stk.pop();
else stk.push(s[i]);
}
return stk.empty();
}
};