剑指链表总结。
剑指 Offer 06. 从尾到头打印链表
可以用朴素的数组记录,最后倒序一下就好了。
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> ans;
if(head == nullptr){
return ans;
}
while(head!=nullptr){//遍历,都加到数组里面
ans.push_back(head->val);
head = head->next;
}
reverse(ans.begin(),ans.end());//倒序一下!
return ans;
}
};
当然也可以用栈。
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> ans;
stack<ListNode*> mystack;
if(head == nullptr){
return ans;
}
while(head!=nullptr){//遍历链表,节点放栈里
mystack.push(head);
head = head->next;
}
while(!mystack.empty()){//从栈里依次取出
ans.push_back(mystack.top()->val);
mystack.pop();
}
return ans;
}
};
剑指 Offer 22. 链表中倒数第k个节点
最好的办法就是快慢指针,让快指针领先慢指针k-1步,随后一起出发,快指针到头时慢指针指向的就是答案。
为什么是先走k-1步呢?因为题目中k的设定和程序常见的设定不同,最后一个节点不是倒数第0而是倒数第1,所以快指针要少走一步。
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
if(head == NULL) return head;
ListNode* fast = new ListNode(0);
fast->next = head;
ListNode* slow = new ListNode(0);
slow->next = head;//创立快慢指针,都指向头结点
for(int i=1;i<k;i++){//快指针先移动个k-1次。
fast = fast->next;
}
while(fast->next!=NULL){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
剑指 Offer II 024. 反转链表
反转链表,典!
核心思路是,每次只改变一个节点的一个指向,不要贪心,改变了之后再更新pre和head,直到head到达空节点。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr;
while(head!=nullptr){
ListNode* temp = head->next;//记录cur的下一个节点是为了cur位置的移动,因为cur->next之后变了
head->next = pre;//每次只改变 cur pre之间的关系
pre = head;//更换pre cur的位置
head = temp;
}
return pre;//cur到了空节点,所以返回pre
}
};
剑指 Offer 25. 合并两个排序的链表
合并排序链表也很典。
核心思想是留下个哑结点当头部索引,用一个节点去不断扩展链表节点。
这里为什么要用pre->next遍历而不是cur,是因为这样省去了判断头结点该是哪个链表头结点的麻烦。
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* prehead = new ListNode(0);//用于标识头部的哑指针
ListNode* pre = prehead;//用于遍历,构建新链表的指针
while(l1!=nullptr&&l2!=nullptr){//如果有一个链表空了就不用比了
if(l1->val < l2->val){//注意是用pre->next遍历
pre->next = l1;
pre = pre->next;
l1 = l1->next;
}else{
pre->next = l2;
pre = pre->next;
l2 = l2->next;
}
}
if(l1){//处理剩余串!
pre->next = l1;
}
if(l2){
pre->next = l2;
}
return prehead->next;
}
};
剑指 Offer 35. 复杂链表的复制
复杂链表复制,我们采用哈希表的方式,记录新链表节点和老链表节点的对应关系。
这里用到了一个哈希表,哈希表的key是老链表的节点,value是新链表上与该老链表节点对应的节点(也就是val值一样)。
最后的效果是hashmap[newnode]
就是老链表上的节点newnode
对应的新节点。
所以新链表节点的下一个节点hashmap[newnode]->next
就是hashmap[newnode->next]
,即老链表对应的节点的下一个节点对应的新节点。
稍微有点绕可以多想下。
class Solution {
public:
Node* copyRandomList(Node* head) {
unordered_map<Node*,Node*> hashmap;//哈希键值对key是老链表当前节点,value是新链表的当前节点,两个节点的val是相等的
Node* newnode = head;//head的一个复制品
while(head != NULL){//遍历链表
Node* temp = new Node(head->val);
hashmap[head] = temp;//构建键值对,key是当前节点,value是一个当当前节点值相同的新节点,即拷贝节点
head = head->next;
}
head = newnode;//给head重定位到初始位置,head要当最终答案的头节点索引,所以head不去动了,动newnode
//给拷贝的新节点进行组装,组装成一个新链表
while(newnode != NULL){//遍历链表
hashmap[newnode]->next = hashmap[newnode->next];//等号左边 右边都是拷贝节点
hashmap[newnode]->random = hashmap[newnode->random];
newnode = newnode->next;
}
return hashmap[head];
}
};
剑指 Offer 52. 两个链表的第一个公共节点
这个题的思路就在图里面。
如果两个链表确实有交点的话,我们可以抽象将这两个链表拼接起来,然后同时遍历一定能找到相交的节点。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA==NULL||headB==NULL){
return NULL;
}
ListNode* cur1 = headA;//定义两个节点,分别对两个链表遍历
ListNode* cur2 = headB;
while(cur1 != cur2){//如果不相同就继续遍历
cur1 = cur1==NULL?headB:cur1->next;//如果走到了空节点就从另一个链表的起点开始!
cur2 = cur2==NULL?headA:cur2->next;//如果没走到空节点就先遍历着自己!
}
return cur1;
}
};
判断链表是否有环
判断是否有环,典!
class Solution {
public:
bool hasCycle(ListNode* head) {
if(head==NULL||head->next==NULL){
return false;
}
ListNode* slow = head;//慢指针初始化
ListNode* fast = head->next;//fast要和slow初始的时候错开
while(fast->next!=NULL&&fast->next->next!=NULL){//fast在前,如果没环肯定是fast先碰到空节点
if(fast==slow) return true;//如果相撞说明有环
fast = fast->next->next;//快指针一次移动两步,所以需要提前两步判断是否要结束
slow = slow->next;//慢指针一次移动一步
}
return false;
}
};
剑指 Offer II 022. 链表中环的入口节点
这题蛮有意思,明显感觉和是刚刚的判断是否有环有关。
这道题已经告诉我们有环了,所以我们使用判断环的函数得到环里的一个节点。不管这个节点是哪个,我们至少清楚它在环里面。
在环里面,我们让它在环里走一圈,就能得出环的长度。
通过对图的观察我们发现,如果使用快慢指针,让快指针先走n步,n是环的长度,两个再一起同速走,最后会在环的起点相遇。
class Solution {
public:
ListNode* getpoint(ListNode* head){//这个函数是判断有无环的,返回环里面的一个节点
if(head==NULL||head->next==NULL) return NULL;
ListNode* fast = head->next;
ListNode* slow = head;
while(fast->next!=NULL&&fast->next->next!=NULL){
if(fast == slow) return fast;
fast = fast->next->next;
slow = slow->next;
}
return NULL;
}
ListNode *detectCycle(ListNode *head) {
ListNode* point = getpoint(head);
if(point==NULL) return NULL;
int count = 1;
for(ListNode* node = point;node->next!=point;node=node->next){//遍历链表,计算环的长度
count++;
}
ListNode* fast = head;
ListNode* slow = head;
while(count--){//快指针先走过环的长度
fast = fast->next;
}
while(slow!=fast){//同速遍历然后相遇!
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
剑指 Offer 18. 删除链表的节点
删除定值节点,核心其实是哑结点,有了哑结点可以有效解决头结点是否会删除的问题
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
ListNode* dummy = new ListNode(0);
ListNode* pre = dummy;
dummy->next = head;
while(head!=NULL){
if(head->val == val){//如果head需要删除,就让pre直接指向head的下一个节点
head = head->next;
pre->next = head;
}else{//用pre和head遍历链表
pre = head;
head = head->next;
}
}
return dummy->next;
}
};
删除排序链表中的重复元素
说到删除链表节点,这不得不喊起来删除重复元素两兄弟。
第一个兄弟是删除重复的链表节点,令每种只剩一个。
只剩一个实际上非常好想,我们直接遍历链表,如果当前节点的下一个节点值和当前节点相等,我们就一直删掉下一个节点就好了,最后会将当前节点当做剩下的重复节点。
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if(head==nullptr) return head;
ListNode* cur = head;//按思路头结点不可能删除,不需要哑指针!
while(cur->next!=nullptr){//当前节点一直和下一个比
if(cur->val==cur->next->val){//如果遇到了一样的
int temp = cur->val;
while(cur->next!=nullptr&&cur->next->val==temp){//就把后面一样的都删了
cur->next=cur->next->next;
}
}else{
cur=cur->next;
}
}
return head;
}
};
删除排序链表中的重复元素 II
第二个重复元素删除他来了。
这一次要求就比较绝绝子,要把相同的元素斩草除根。
首先头结点可能是保不住了,这个题肯定是要用哑指针的。
遇到这种头结点难保的题,其实我们可以用一种稍微定式一些的思路:我们用pre->next遍历链表而不是cur,这样等于是保留了当前节点的前一个节点,这样我们删除当前节点就很方便了!
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (!head || !head->next)
return head;
ListNode* dummy = new ListNode(-1);//设置哑节点
dummy->next = head;//哑结点下一个要指向头结点
ListNode* pre = dummy;//cur初始化为哑结点,今后的数据比较都是在cur->next和cur->next->next中进行
while(pre->next&&pre->next->next){
if(pre->next->next->val == pre->next->val){//出现重复节点
int temp = pre->next->val;
while(pre->next&&pre->next->val==temp){//一直跳过重复节点
pre->next = pre->next->next;//等于是不断删除当前节点,体现了pre->next遍历的好处!
}
}else{
pre = pre->next;
}
}
return dummy->next;
}
};
K 个一组翻转链表
链表里面也有狠角色这里补充一下。
拿笔画一画就知道,这个题如果用迭代会很困难。
所以这里用到了链表的迭代法。
该迭代的核心思想是先检测能不能凑够k个,如果凑不到就直接返回这一部分的头部;如果能凑够就翻转从head开始的k个节点,此时head是该部分的尾节点,然后通过递归得到下一部分的头结点并加入到当前的尾结点后方。
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* last = head;//这一块用来计算剩下节点能不能到k?如果不能到k直接返回head
for(int i=0;i<k;i++){
if(last==nullptr) return head;
last = last->next;
}
//如果剩下的节点够的话,就把这一组反转了
ListNode* pre = head;
ListNode* cur = head->next;
for(int i=0;i<k-1;i++){//因为head是pre,所以只翻k-1次!
ListNode* temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
//看出来了吗,本质是链表的后序遍历,从尾向头
ListNode* next = reverseKGroup(cur,k);//递归得到下一段k个节点反转后的头结点,如果不到k就直接拿原链表
head->next = next;//当前的head是反转以后的尾节点,给它接上后面的
return pre; //反转以后头结点是pre,将其返回
}
};