第一题(对应LeetCode题库的第141题)
(当然,后续if自己看不懂自己的总结,可以去力扣网站翻阅回对应的题目去看题解!)
(leetcode刷题最好看英文版的题目描述,不然又会给傻逼的语文没学好的人翻译过来的题目文字搞到我看不懂了直接!!!)
题目:给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
下面是示例:
解法一:
思路:利用unordered_set哈希表(也即无序的set)来遍历存储单向list中的各个节点,一旦发现其中的某个节点已经存在于之前所存储的unordered_set中,就立即判断出这是一个环状单向list,否则则不是。
其实本质上这里就和你在一个一维int型数组这种数据结构, int a[] = {1,2,3,4,5,2,3,8,0,9,10} 中去查找是否有重复的int型元素是一样的思路,只不过这里换成了在容器unordered_set哈希表这种数据结构中去查找是否存在重复的一个指针类型(ListNode*)而已!要把思路抽象出来,这才是刷leetcode题的本质,也即去联系你的把问题抽象出来,变成一个一般化的问题,然后逐个去击破去解决!
key code:unorderedSet.count(head)
如果出现过,则返回非0,也即true,若没出现过,则返回0,也即false。
请看以下正确代码:
class Solution {
public:
//对于单向环状链表而言,必须是从last一个节点往回指向的!
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> unorderedSet;
while(head != nullptr){
// if(unorderedSet.find(head)!=unorderedSet.end()){
// return true;
// }
if(unorderedSet.count(head)){
return true;
}
unorderedSet.insert(head);
head = head->next;
}
return false;
}
};
解法二:
思路:利用快慢指针(双指针)来解决单链表list是否有环的问题。
某个大佬说的:这种题目在牛客上刷专项的时候遇到过一两次,这种题目最快的解决办法使快慢指针法,设置两个指针,一个跑得快一个跑得慢,要是快的那一个追上了慢的那一个,就说明,这是一个循环链表,否则不是
当list中不存在环状时:只有如下2种case:
1、head == nullptr or head->next == nullptr
2、fast== nullptr or fast->next == nullptr
当list中存在环状时:只有如下1种case:
也即 slow == fast
key code:
if(fast == nullptr || fast->next == nullptr){
return false;
}
//但凡是fast == nullptr or fast->next == nullptr 就一定不是环状的单list
slow = slow->next;//慢指针只会移动一个单位
fast = fast->next->next;//快指针会移动两个单位
请看以下正确代码:
class Solution {
public:
//对于单向环状链表而言,必须是从last一个节点往回指向的!
bool hasCycle(ListNode *head) {
if(head == nullptr || head->next == nullptr){
return false;//这是单list中只有1个元素的case
}
ListNode* slow = head;
ListNode* fast = head->next;
while(slow != fast)
{
if(fast == nullptr || fast->next == nullptr){
return false;
}
//但凡是fast == nullptr or fast->next == nullptr 就一定不是环状的单list
slow = slow->next;//慢指针只会移动一个单位
fast = fast->next->next;//快指针会移动两个单位
}
return true;
}
};
第二题(对应LeetCode题库的第206题)
(当然,后续if自己看不懂自己的总结,可以去力扣网站翻阅回对应的题目去看题解!)
题目:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
解法一:
思路:迭代(下面举一个具体的例子用一张图以毕之)
请看以下正确代码:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prevNode = nullptr;
ListNode* currentNode = head;
while(currentNode){
ListNode* nextNode = currentNode->next;
currentNode->next = prevNode;//这个prev就是用来遍历记录(指向)list元素的!
prevNode = currentNode;
currentNode = nextNode;
}
return prevNode;
}
};
解法二:
思路:递归(当然现在刚好是2021-10-1,我没有完全掌握递归的用法和精髓,等后续学习的差不多了就回过头来用递归来解决这道题!)
第三题(对应LeetCode题库的第21题)
(当然,后续if自己看不懂自己的总结,可以去力扣网站翻阅回对应的题目去看题解!)
题目:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解法一:
(通过散列表multimap<int, ListNode*, greater<int>> mmp)来存储2个list中的all数据,当然中间因为涉及到从后往前push进newList,因此就需要把mmp做一个降序的排序(其默认是升序的),最终再把已经排好序的mmp逐个地插入到新的newlist中,return newlist即可。
#include<iostream>
#include<algorithm>
#include<map>
#include<functional>
using namespace std;
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* konList = nullptr;
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* newlist = nullptr;
if (l1 == nullptr && l2 == nullptr)
{
cout << "此时l1 and l2都为空!" << endl;
return newlist;
}
else if (l1 == nullptr) {
cout << "此时l1为空!" << endl;
newlist = l2->next;
return newlist;
}
else if (l2 == nullptr) {
cout << "此时l2为空!" << endl;
newlist = l1->next;
return newlist;
}
else {
//直接用一个临时的散列表multimap就可以deal了
multimap<int, ListNode*, greater<int>> mmp;
while (l1 != nullptr)
{
cout << "list1的元素: " << l1->val << endl;
mmp.insert({ l1->val, l1 });
l1 = l1->next;
}
while (l2 != nullptr)
{
cout << "list2的元素: " << l2->val << endl;
mmp.insert({ l2->val, l2 });
l2 = l2->next;
}
while (mmp.count(0))
{
mmp.erase(0);
}
multimap<int, ListNode*, greater<int>>::const_iterator it = mmp.begin();
ListNode* newtempNode = new ListNode;
newtempNode->next = it->second;
cout << newtempNode->next->val << endl;
while (it != mmp.end())
{
it++;
if (it == mmp.end()) {
break;
}
//从后往前插入,那么tempNode最终会处于头结点的位置了!
ListNode* newNode = new ListNode(*(it->second));
cout << "当前插入的元素是:" << it->first << endl;
newNode->next = newtempNode->next;
newtempNode->next = newNode;
cout << "插入元素:" << newNode->val << "成功!" << endl;
}
newlist = newtempNode->next;
return newlist;
}
}
};
void test()//test codes 都放在这里了
{
Solution s;
ListNode* newList1 = nullptr; //= new ListNode;
ListNode* newListHead1 = new ListNode;
//newList1 = newListHead1;
ListNode* newList2 = nullptr;//= new ListNode;
ListNode* newListHead2 = new ListNode;
newList2 = newListHead2;
ListNode* newListAfterMerge = nullptr;// = new ListNode;
ListNode* newNode1 = new ListNode(11);
ListNode* newNode2 = new ListNode(12);
ListNode* newNode3 = new ListNode(13);
ListNode* newNode4 = new ListNode(0);
ListNode* newNode5 = new ListNode(22);
ListNode* newNode6 = new ListNode(23);
//newList1->next = newNode1;
//newNode1->next = newNode2;
//newNode2->next = newNode3;
//if (newList1 != nullptr) {
// cout << "这表明即使是带val == 0的节点也不为空的节点!" << endl;
// cout << "newList1->val = " << newList1->val << endl;
//}
newListHead2->next = newNode4;
//newNode4->next = newNode5;
//newNode5->next = newNode6;
/*newList1 = newList1->next;*/
//while (newList1 != nullptr) {
// cout << "list1的第一个元素:" << newList1->val << endl;
// newList1 = newList1->next;
//}
//newList2 = newList2->next;
//while (newList2 != nullptr) {
// cout << "list2的第一个元素:" << newList2->val << endl;
// newList2 = newList2->next;
//}
int i = 0;
newListAfterMerge = s.mergeTwoLists(newList1, newList2);
while (newListAfterMerge != nullptr) {
cout << "第" << i + 1 << "个元素:" << newListAfterMerge->val << endl;
newListAfterMerge = newListAfterMerge->next; i++;
}
}
int main(void)
{
test();
system("pause");
return 0;
}
运行结果可以自行到VS2019中去跑即可,反正在leetcode上这个方法对应某一组输入还是有错误!但是我在VS上抛的时候一点问题没有,这个问题待以后coding学习再回头deal它!
解法二:
用迭代法,结合头结点,逐个对比2个list中的数据的大小,把2个list中的较小者放入newlist中,最后一定是会有这样的结果:
1---list1空,list2非空
2---list1非空,list2空
3---list1空,list2也空
也即list1和list2中之多有一个为非空,那么此时就可用一个三目运算符把剩下的已经有序了的list插入回newlist中就完事了!
请看以下代码:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* headNode = new ListNode(-1);
ListNode* tempNode = new ListNode;
tempNode = headNode;
while(l1 != nullptr && l2 != nullptr)
{
if(l1->val < l2->val){
tempNode->next = l1;
l1 = l1->next;
}else{
tempNode->next = l2;
l2 = l2->next;
}
tempNode = tempNode->next;
}
tempNode->next =(l1 == nullptr ? l2 : l1);
return headNode->next;
}
};
运行结果:
解法三:
用递归的方法do,因为meet递归使用的三大条件:(其实写递归代码时,我们只需要关注递归的递推公式+递归结束条件即可,用代码实现以上两项内容即可deal问题,无需钻进去了解递归调用每一层的细节,我们可以举个例子比如2层的例子钻进去稍微了解一下有利于写代码即可,无需搞的很complex!)
1--原问题可以分解为若干子问题:每次都比较2个list中的头结点的大小,比出结果就往更小的list,再进行比较
2--存在递归结束条件:当list1orlist2为空时,那就会结束循环
3--子问题和各层原问题除了规模不同之外,解法完全一致
请看以下代码:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) {//递归结束条件1
return l2;
} else if (l2 == nullptr) {//递归结束条件2
return l1;
} else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);//分解为子问题
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);//分解为子问题
return l2;
}
}
};
运行结果:
第四题(对应LeetCode题库的第876题)
(当然,后续if自己看不懂自己的总结,可以去力扣网站翻阅回对应的题目去看题解!)
题目:给定一个头结点为 head
的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
解法一:
我们可以对链表进行两次遍历。第一次遍历时,我们统计链表中的元素个数len;第二次遍历时,我们遍历到第 len/2 个元素(链表的首节点为第 0 个元素)时,将该元素返回即可。
1-奇数个结点 titalNumbers/2 == 对应的中间节点的index
2-偶数个结点 (hi-lo)/2 ==对应的第一个中间节点的index
2-偶数个结点 (hi-lo)/2 + 1==对应的第二个中间节点的index
做遍历时,你会发现无论是奇数还是偶数,最终head都会遍历到符合题目意思的位置,都只需要遍历len/2次即可,当然index要从0开始。
请看以下代码:
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* tempNode = head;
int len = 0;
while (tempNode != nullptr) {//求总人数
cout << tempNode->val << endl;
len++; tempNode = tempNode->next;
}
tempNode = head;
cout << "一共有:"<<len<<"个元素!" << endl;
int index = 0;
while (index < len / 2) {
tempNode = tempNode->next; index++;
}
return tempNode;
}
};
解法二:
当然你还可以用vector数组本来保存list中的元素,最终返回vec[len/2]的元素也行
根本思路还是:我们可以对链表进行两次遍历。第一次遍历时,我们统计链表中的元素个数len;第二次遍历时,我们遍历到第 len/2 个元素(链表的首节点为第 0 个元素)时,将该元素返回即可。
请看以下代码:注意:(cout输出的东西都是我的测试代码!)
class Solution {
public:
ListNode* middleNode(ListNode* head) {
vector<ListNode*> vec;
ListNode* tempNode = head;
int len = 0;
while (tempNode != nullptr) {//求总人数
cout << tempNode->val << endl;
vec.push_back(tempNode);
len++; tempNode = tempNode->next;
}
cout << "一共有:" << len << "个元素!" << endl;
return vec[len / 2];
}
};
运行结果:
第五题(对应LeetCode题库的第19题)
(当然,后续if自己看不懂自己的总结,可以去力扣网站翻阅回对应的题目去看题解!)
题目:
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
方法一、在遍历链表list的同时使用一个临时的stack栈来记录list中的元素,在stack中对元素进行删除的操作,然后再将stack中的元素赋值给一个newlist并返回即可!
(你既可以用双栈实现,也可以用单栈实现(当然,单栈实现的way比较好!)这是我自己想的)
请看以下的双栈实现代码:
//双栈法
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
stack<int> inStk, outStk;
ListNode* tempList = new ListNode(*head);
while ( tempList != nullptr) {
inStk.push(tempList->val);
tempList = tempList->next;//遍历
}
if(tempList != nullptr){
delete tempList;
tempList = nullptr;
}
int count = 0;
while (!inStk.empty())
{
outStk.push(inStk.top());
inStk.pop();
count++;
if (count == n) {
outStk.pop();//删除元素
}
}
//将元素从outStk搬移回去inStk中去
while (!outStk.empty()) {
inStk.push(outStk.top());
outStk.pop();
}
//重新构成一个list
ListNode* newList = nullptr;
ListNode* newHead = new ListNode;
newList = newHead;
while (!inStk.empty()) {
ListNode* newNode = new ListNode;
newNode->val = inStk.top();
inStk.pop();
newNode->next = newHead->next;
newHead->next = newNode;
}
return newList->next;
}
};
请看以下的单栈实现代码:
//单栈法:
class Solution {
private:
int getLength(ListNode* head)
{
int len = 0;
ListNode* tempList = new ListNode(*head);
while (tempList != nullptr) {
len++;tempList = tempList->next;
}
return len;
}
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
stack<int> inStk;
int count = 0;
int Size = getLength(head);
cout << "Size == " << Size<< endl;
ListNode* tempList = head;
while (tempList != nullptr) {
cout << "当然插入stk的元素:" << tempList->val << endl;
inStk.push(tempList->val);
if ((Size - count) == n) {
cout<<"删除元素:"<< inStk.top()<<endl;
inStk.pop();
}
count++;
tempList = tempList->next;//遍历
}
//重新构成一个list
ListNode* newList = nullptr;
ListNode* newHead = new ListNode;
newList = newHead;
while (!inStk.empty()) {
ListNode* newNode = new ListNode;
cout << "插入元素:" << inStk.top() << "形成新的list!" << endl;
newNode->val = inStk.top();
inStk.pop();
newNode->next = newHead->next;
newHead->next = newNode;
}
return newList->next;
}
};
运行结果都差不多是:
当然,leetcode官方给出的用栈实现的代码也值得我去深究!(运行结果和上图无差)
请看:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
stack<ListNode*> myStk;
//通过在遍历链表list的同时,将链表中的all的节点元素都do进栈操作
ListNode* tempList = new ListNode(0,head);
ListNode* cur = tempList;
//这里一定要把我没呢临时做的头节点包含进去,否则当head只有一个节点时,就会导致在下面的mstk.pop()之后无法形成新的链表导致编译器报错!
while (cur != nullptr)
{
cout << cur->val << endl;
myStk.push(cur);
cur = cur->next;
}
for (auto i = 0; i < n; i++) {
myStk.pop();
}
ListNode* prevNode = myStk.top();
prevNode->next = prevNode->next->next;//delete要删除的元素
ListNode* newList = tempList->next;
delete tempList; tempList = nullptr;
return newList;
}
};
其中:
ListNode* cur = tempList;
这一行代码尤为关键!
因为若当head指向的链表list中只有一个节点时,就会导致在下面的mstk.pop()之后无法形成新的链表导致编译器报错!也即我当前这种代码算法就没法解决这种边界case。
(除非你再添加一些条件判断codes来deal这种边界问题!当然这样做不仅难以囊括all的特殊边界case而且还会增加空间复杂度,这样do是不好的选择!)
因此来一个临时的头节点,也即这一行代码也是非常关键的。
ListNode* tempList = new ListNode(0,head);
这里的坑是写链表算法题的常见的坑!一定要自己慢慢地仔细地品味才能领会其中的奥秘和关键点所在! 后期coding中如果再遇到此类case可以再翻阅这一题的notes!
so此后对于链表的任何操作除了定义一个newlist = new ListNode(0,head) 还需要一个newHead = newlist;//让这个newhead所指向的链表是一个拥有临时头节点的链表!临时头节点这个key会伴随着all的链表题,我务必要有这个概念!以后写链表lc题时,我先毫不犹豫写下这2行代码:
ListNode* newList = new ListNode(0,head);
ListNode* newHead = tempList;
你只要在返回ListNode* 时返回newList->next即可不把我们在题目中创建的所谓的临时头节点
包含进去了!也即把题目想要的拥有正确节点元素的链表返回回去!
只要一见到关于list的题目就把这2行先写上!一定是会有用的!
记住:Learning is practice again and again!代码写千万遍,其意自现!
方法二、
(快慢指针法,当然这里并不是指fast和slow移动的步伐不一致,其实他们都是一步移动一个位置的,只不过fast先行移动,slow后行移动而已)用两个指针fast和slow,分别指向head和head的前一个位置,然后通过画图分析,我可以清楚地知道只有当cnt(从0开始)这个计数变量计数到>= n时即可让slow指针开始移动了(一步移动一个位置),当fast指向nullptr(也即原链表遍历完毕后)即可让slow->next = slow->next->next了,这样既可实现删除对应的倒数第n个位置的链表元素的目的了。
总结而言这个快慢指针法其实就是要你去找每一个题目的规律而已,找到了自然很容易写出来了!
请看以下的快慢指针法的实现代码:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* newList = new ListNode(0,head);
ListNode* slow = newList;//慢指针
ListNode* fast = head;//快指针
int cnt = 0;
while (fast != nullptr) {
if (cnt >= n) {
slow = slow->next;
}
fast = fast->next;
cnt++;
}
slow->next = slow->next->next;
newList = newList->next;
return newList;
}
};
运行结果大致是:
第六题(对应LeetCode题库的第203题)
题目:移除链表元素(好题!!!)
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
解题思路:
以后我但凡是遇到关于链表的题目,一定要先自己new一个虚拟的头节点,然后用这个头节点去do事情!
这里我直接设置虚拟头节点然后用遍历的方法来do即可了,不难!
代码:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//一般的链表题目都需要我们有添加一个虚拟头结点的处理方法!
//以后做关于链表的题目时,最好都自己构造一个虚拟的头节点来做处理!
ListNode* virtualHead = new ListNode;
virtualHead->next = head;
ListNode* tempNode = virtualHead;
while(tempNode->next!=nullptr){
if(tempNode->next->val == val){
tempNode->next = tempNode->next->next;
}else{
tempNode = tempNode->next;
}
}
return virtualHead->next;
}
};
运行结果:
解题思路2:
递归法,通过递归,不断地让栈帧压入系统栈中,逐个逐个地判断当前head是否为指定值val,若是,则让return head->next,也即让上一个node指向值等于给定值的nextNode,这样通过递归即可把值等于给定值的allNode都跳过去了(也即从链表中给他删除掉了的意思)
代码:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == nullptr){
return head;
}
head->next = removeElements(head->next,val);
if(head->val == val){
return head->next;
}else{
return head;
}
}
};
运行结果: