链表专题:
题目简介 | LeetCode题号 |
---|---|
1-电话号码的字母组合 | LeetCode第17题 |
2-单词搜索 | LeetCode第79题 |
3-全排列 | LeetCode第46题 |
4-全排列Ⅱ | LeetCode第47题 |
5-子集 | LeetCode第78题 |
6-子集Ⅱ | LeetCode第90题 |
7-组合总和Ⅲ | LeetCode第216题 |
8-N皇后Ⅱ | LeetCode第52题 |
9-解数独 | LeetCode第37题 |
10-火柴拼正方形 | LeetCode第473题 |
1.LeetCode-17-电话号码的字母组合
题目描述:
题目分析:
LeetCode里面深度搜索的题目试比较多的,宽度搜索就比较少,所以这里也是重要讲深度搜索。搜索的话,可以用循环来实现,也可以用递归实现,所以深度搜索!=递归。这个题目,yy想实现的是用循环而不是用递归的形式来做。
代码:
class Solution {
public:
// 首先要把每个数字代表
string chars[8] = {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"}; // 下标是从 0 开始的,chars[0]是数字2下面的字母
vector<string> letterCombinations(string digits) {
if(digits.empty()) return vector<string>(); // 这里的返回值为什么加了(),不是很理解;
vector<string> state(1, ""); //表示盛放字符串的数组中只有一个字符串,是空;
for(auto u : digits){ // 1. 首先要循环每一个数字
vector<string> now; // 2. 用于保存新组合的字符串
for(auto c : chars[u - '2']){ // 3. 将当前数字下的字母取出来
for(auto s : state){ // 4. 遍历原来的字符串
now.push_back(s + c); // 加入新的字母
}
}
state = now; // 更新字符串
}
return state;
}
};
2-LeetCode-79-单词搜索
题目描述:
题目分析:
上面是题目的基本含义。该题是一个搜索题,搜索题最重要的是一个顺序问题。需要注意一点的是,我们在搜索过程中,只能往前或者说往下去搜索,而不能回头搜索。
- 枚举起点;
- 起点确定之后,从起点开始依次搜索下一个节点的位置(第二个点在搜索的时候就只能向后、向左、向右进行,不能向前进行)时间复杂度:nm * 3^k :其中nm分别代表行数和列数,路径的平均长度是k(也就是我们要搜索的单词的长度),3指的是每次有三个方向是可以选择的。在搜索的过程中主要是判断这个路径是不是合法的,是否合法就是要判断当前的路径是不是和我们要搜索的字母是一致的。
代码:
class Solution {
public:
int n , m ; // 全局变量,分别表示行和列
// 下面是枚举四个方向的技巧:隐约觉得这个方向数组用的很巧妙,实际上是用1 和 -1 来分别表示前进或者后退,然后用 0 表示不动,不太明白的是这个顺序:【0:左】【1:下】【2:右】【3:上】
int dx[4] = {-1 , 0 , 1 , 0} , dy[4] = {0 , 1 , 0 , -1 };
bool exist(vector<vector<char>>& board, string word) {
// 特别判断一下,如果这个盒子为空,或者列为空,返回false
if(board.empty() || board[0].empty()) return false;
n = board.size() , m = board[0].size();
for(int i = 0 ; i < n ; i++)
for(int j = 0 ; j < m ; j++)
if(dfs(board , i , j , word , 0))
return true;
return false;
}
// 写一个深度搜索的函数,其中x,y表示我们当前走到了那个格子了,word是要找的目表单词,u是我们找到目标单词的第几位了,这里传递参数board的时候传的是取地址的,因为这样的话,直接去地址找到这个数组,不需要将数组一遍又一遍的复制了
bool dfs(vector<vector<char>>& board , int x , int y , string& word , int u){
// 如果不匹配的话:
if(board[x][y] != word[u]) return false;
if(u == word.size() - 1) return true; //如果寻找到了单词的最后一个位置,那么就成功了;
board[x][y] = '.'; // 如果我们当前这个格子我们用过了,那么就不能再用了,这里就需要做一个标记;
// 这里的大部分代码都是需要回溯的,所谓的回溯就是恢复初始状态
for(int i = 0 ; i < 4 ; i++){
int a = x + dx[i] , b = y + dy[i];
if(a >= 0 && a < n && b >= 0 && b < m)
if(dfs(board , a , b , word , u + 1))
return true;
}
board[x][y] = word[u]; //这个应该就是所谓的回溯,恢复了初试状态,在进行初试状态之前还递归了很多次;这里恢复现场就是要保证在走不同的路径的时候,看到的初试状态是一致的;
return false;
}
};
3-LeetCode-46-全排列
题目描述:
题目分析:
3 – 5 是一个类型的题目,排列组合的枚举。一直在强调怎么样才能不遗漏枚举出来?关键就是顺序!枚举每一个位置上该放那个数;或者是枚举每一个数放在那个位置上。下面是这两种方式的分析,体会一下:
代码:
class Solution {
public:
// 实现的是枚举每一个位置:
int n ;
vector<bool> st ; // 用来保存当前这个分支用的数字是那些
vector<vector<int>> ans; // 用来存所有的方案
vector<int> path ; // 用来存当前的方案的
vector<vector<int>> permute(vector<int>& nums) {
n = nums.size() ;
st = vector<bool>(n) ;
dfs(nums , 0);
return ans;
}
void dfs(vector<int>& nums , int u){
if(u == n){ // 判断一下边界,如果都遍历完了,就插入到最终的结果中
ans.push_back(path);
return ;
}
// 否则枚举一下当前这个格子可以填进去那个数:
for(int i = 0 ; i < n; i++)
if(!st[i]){ // 当st为false的时候,表示可以填
st[i] = true;
path.push_back(nums[i]);
dfs(nums , u + 1);
path.pop_back(); // 这个是恢复现场
st[i] = false;
}
}
};
4-LeetCode-47-全排列Ⅱ
题目描述:
题目分析:
全排列的这个题目的话,是有两种方式:枚举每个位置上存放那个数;枚举每个数存放在哪个位置上。全排列的这个第Ⅱ个题目,用枚举每个数存放在那个位置(这里有重复的元素)。
- 这个全排列一个很重要的事情就是:判重!
- 解决这个问题的时候首先将要处理的是将这个int类型的数组里,相同的数放到一起——用“排序sort来实现”;
- 在全排列的过程中,因为有相同的数,而这个相同的数在前和在后是同样的顺序,为了避免相同的数被排两次,就要认为的规定一个顺序,因为我们之前用sort进行了排序,那么我们现在就要将这个顺序定为:不变,按照原来我们排好的顺序;
- 顺序不变的话,就要在dfs()函数里面多设置一下状态,以前用u来表示每个位置,现在用u来表示每个字母,并且还要dfs(u , start):start来表示当前可以从哪个数开始搜索,简单的来说:如果是1123的话,假设将第一个1放到了第三个位置,那么第2个1就要从第四个位置进行搜。
代码:
class Solution {
public:
// 首先把位置开辟出来:
int n ;
vector<vector<int>> ans;
vector<int> path;
vector<bool> st ; // 状态,应该是用来表示每个数是否被用了
vector<vector<int>> permuteUnique(vector<int>& nums) {
// 先开辟了这些空间
n = nums.size();
st = vector<bool>(n);
path = vector<int>(n);
sort(nums.begin() , nums.end());
dfs(nums , 0 , 0);
return ans;
}
// u 代表当前要存储第 u 个数,start表示当前可以枚举的位置的开始
void dfs(vector<int>& nums , int u , int start){ // start 表示当前从哪个位置开始枚举
// 判断一下是否所有的数字都放下了
if(u == n){
ans.push_back(path);
return ;
}
// 如果有数字还没放下,从start位置开始枚举
for(int i = start ; i < n ; i++){
if(st[i] == false){ // 开始看看每一个位置是否被用了,没有用,继续
st[i] = true;
path[i] = nums[u];
// 在进行继续枚举下一个数放到哪里的时候,需要判断一下,下一个数是否和当前数相同:
dfs(nums , u + 1 , u + 1 < n && nums[u + 1] == nums[u] ? i + 1 : 0);
st[i] = false; // 恢复现场
}
}
}
};
5-LeetCode-78-子集
题目描述:
题目分析:
做法有两种,一个是递归,一个是循环。递归的话最重要的就是顺序。这里用一个比较新的方法,迭代的写法,利用到了2进制的写法;是一个特别巧妙的写法,如下面展示的,先假设数字只有三个,那么它所有的子集合如下面的所示,一共有8个,是2^n – 1 (n = 3)个,那么可以用二进制表示,如下面所展示的,需要注意的点是:001表示个位数是1,各位是1,2,3中的1;这里需要注意的一点就是,如何找到每一位上的0或者是1?实现:
i >> j & 1
代码:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
for(int i = 0 ; i < 1 << nums.size() ; i++){ // nums.size()是一个数,左移1变成
vector<int> now; // 定义一个暂时的数组来存放一个子集合;
for(int j = 0 ; j < nums.size() ; j ++)
if(i >> j & 1) // i 先右移 j 位后再做与
now.push_back(nums[j]);
res.push_back(now);
}
return res;
}
};
这里用了一个左移一个右移,我必须整明白这两的的用法
6-LeetCode-90-子集Ⅱ
题目描述:
题目分析:
在上一个题目中,我们需要考虑的是这个数在集合中选择还是不选择,90这个题目里面有重复的数字,我们就需要考虑 每个数字选择几次,这里列举了一下 1,2,2,2,3,3这么几位数,首先是统计每个数的状态(出现的次数)
- 1:0(不出现),1(出现);
- 2:0(两个1都不出现),1(第一个1出现),2(第二个1出现),3(两个1都出现);
- 3:0,1,2;
这个题目最重要的是要想明白各个指针是怎么指向的,以及如果判断结束,这个我认为是重点。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head) return NULL;
auto a = head,b = head->next;
while(b)
{
auto c = b->next;
b->next = a;
a = b;
b = c;
}
head->next = NULL;
head = a;
return head;
}
};
总结一下上面的几个代码的话,几乎就是用两个或三个指针来完成的。
7-反转链表- LeetCode第92题
题目描述:
题目分析:
上面就是一个解题的思路,做这个题目的话就是分两步去做,首先是把m->n之间的节点反转过来,然后再把反转之后的一段节点插到原来的序列中去,但是需要提前找到a、m、n、c这是个节点。经过整数值m,走m-1步可以找到指针a;通过整数n走n步到达指针d,然后接着next就可以找到指针b、c。 这个题目头节点时可能会被反转,因为头节点可能会变化,所以这里搞一个虚拟的头节点。指针的设置如上面,这个指针如果整明白了,那么这个题目就会好做很多,但是那个指针指向那个节点一定一定要好好的盯着,很容易出错,思路上都是不难的。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
if(m==n) return head;
auto dummy = new ListNode(-1);
dummy->next = head;
auto a = dummy,d = dummy;
for(int i = 0; i < m - 1; i++) a = a->next;
for(int j = 0; j < n; j++) d = d->next;
auto b = a->next,c = d->next;
for(auto p = b,q = p->next;q != c;)
{
auto o = q->next;
q->next = p;
p = q;
q = o;
}
b->next = c;
a->next = d;
return dummy->next;
}
};
/**
//这个代码是指针换的之后的,自己写的
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
auto dummy = new ListNode(-1);
dummy->next = head;
auto a = dummy,c = dummy;
for(int i = 0;i < m-1;i++) a = a->next;
for(int j = 0;j < n; j++) c = c->next;
auto b = a->next,d = c->next;
for(auto p = b, q = b->next;q != d;)
{
auto o = q->next;
q->next = p;
p = q;
q = o;
}
a->next = c;
b->next = d;
return dummy->next;
}
};
8-相交链表 - LeetCode第160题
题目描述:
题目分析:
题解的情况有两种:
这个题目的思路很巧妙,首先看算法的步骤:
- 用两个指针分别从两个链表头部开始扫描,每次分别走一步;
- 如果指针走到NULL,则从另一个链表头部开始走;
- 当两个指针相同时,
(1) 如果指针不是NULL,则指针位置就是相遇点;
(2) 如果指针是NULL,则两个链表不相交;
(3) 访问链表的时间复杂度是o(n)
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto p = headA,q = headB;
while(p != q)
{
if(p) p = p->next;
else p = headB;
if(q) q = q->next;
else q = headA;
}
return p;
}
};
题意很长,做起来也不是很容易,但是想到这个方法的话,就太容易了
9-环形链表 - LeetCode第142题
题目描述:
题目分析:
本题想象它是一个这样的形式,这个题目是快慢指针的一个经典题目,fast指针(红色)每次走两步,second指针(蓝色)每次走一步;当蓝色指针从a点走到b点的时候,它走过的距离是X,那么红色指针应该走的距离是2X,也就是从a点先走到b点,然后在这个圈上又走了X远的距离,我们假设红色指针在C’位置上,假设b点到C’位置的距离是y,那么我们在这个圆圈上找到对称的位置C,b到C的距离也是y,让蓝色指针从b到C点(走了y),那么红色指针要走2y,红蓝指针在C点相遇;相遇之后,把蓝颜色的指针放回到开头(此时红颜色依然在C点),这时候红色指针和蓝色指针每次走一步,然后当蓝颜色指针走到b的时候,红颜色也将走到b,即它们一定在b点相遇。
原因的话,我这么解释:红蓝指针在C点相遇之后,蓝色从a—>b走X到b,红色从C每次走一步,走距离X一定到达b,因为原来从b点出发走X步到了C’,那么现在从C出发(起点退后了y)走X应该到b
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
auto fast = head,slow = head;
while(fast){
fast = fast->next;
slow = slow->next;
if(fast) fast = fast->next;
else break;
if(fast == slow)
{
slow = head;
while(fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
}
return NULL;
}
};
个人觉得代码的难度在于判断条天上面。
10排序链表 - LeetCode第148题
题目描述:
题目分析:
题解:这个题目因为它有各种要求,时间复杂度是O(nlogn),空间复杂度是常数(O(1))所以有很多不能用的方法,而是用了一个自底向上的归并排序。因为快速排序的话是要用递归的写法,递归的写法的话肯定是需要用到系统栈的,只要是用到系统栈的题目,如果是递归N层的,一般需要O(logn)空间,故快速排序的话,时间复杂度是满足要求的,但是空间复杂度是不满足要求的。同样的一般的归并排序,递归写法的话也是需要O(logn)空间复杂度的,也是不满足要求。所以这个题目只有一种写法,即自底向上的归并排序的写法,用一个循环的形式,这样的话就不会用到栈了。
自底向上的归并排序的算法:
代码:
/**
* 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* sortList(ListNode* head) {
int n = 0;
for(auto p = head;p;p = p->next) n++;
auto dummy = new ListNode(-1); //头节点可能会变,所以我们创建一个虚拟的头节点
dummy->next = head;
for(int i = 1;i < n; i *= 2) //每次隔得间隔
{
auto cur = dummy;
for(int j = 0; j + i < n; j += i * 2)//枚举每一段
{
auto left = cur->next, right = cur->next;
for(int k = 0;k < i; k++) right = right->next;
//下面要进行归并
int l = 0,r = 0;
while(l < i && r < i && right)
if(left->val <= right->val)//经典的归并排序
{
cur->next = left;
cur = left;
left = left->next;
l++;
}
else//否则把右边放过来,cur始终指向尾部的意思
{
cur->next = right;
cur = right;
right = right->next;
r++;
}
//下面将两段中没有循环的接起来
while(l < i)
{
cur->next = left;
cur = left;
left = left->next;
l++;
}
while(r < i && right)
{
cur->next = right;
cur = right;
right = right->next;
r++;
}
cur->next = right;
}
}
return dummy->next;
}
};//这个题可以当作一个模版了