链表相关题目-leetcode简单
目录
1. 合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
方法一、利用递归思想
class Solution { public: ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { if(l1==nullptr) return l2; if(l2==nullptr) return l1; if(l1->val < l2->val){ l1->next = mergeTwoLists(l1->next,l2); return l1; }else{ l2->next = mergeTwoLists(l2->next,l1); return l2; } } };
方法二、迭代
class Solution { public: ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { ListNode* newHead = new ListNode(-1); ListNode* p = newHead; while(l1!=nullptr && l2!=nullptr){ if(l1->val <= l2->val){ p->next = l1; l1 = l1->next; }else{ p->next = l2; l2 = l2->next; } p = p->next; } p->next = (l1!=nullptr)? l1:l2; return newHead->next; } };
2. 删除排序链表中的重复元素
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
方法一、由于要删除元素,因此要保存要删除元素的前一个结点
class Solution { public: ListNode* deleteDuplicates(ListNode* head) { if(head==nullptr) return head; ListNode* pre = head; ListNode* curr = pre->next; while(curr){ while(curr && pre->val == curr->val){ ListNode* tmp = curr; curr = curr->next; pre->next = curr; tmp = nullptr; //清除野指针 } pre = pre->next; if(pre) curr = pre->next; else curr = nullptr; } return head; } };
3. 环形链表
给定一个链表,判断链表中是否有环。
方法一、快慢指针
class Solution { public: bool hasCycle(ListNode *head) { if(head==nullptr) return false; //注意空链表,认为是没有环的 ListNode *slow = head; ListNode *fast = head; while(fast){ ListNode *tmp = fast->next; if(tmp){ fast = tmp->next; }else{ return false; } slow = slow->next; if(fast==slow) return true; } return false; } };
方法二、哈希表
通过检查一个结点此前是否被访问过来判断链表是否为环形链表。常用的方法是使用哈希表
class Solution { public: bool hasCycle(ListNode *head) { if(head==nullptr) return false; ListNode *curr=head; set<ListNode*> ss; //注意存的类型是ListNode* while(curr){ if(ss.find(curr)!=ss.end()){ //存在 return true; }else{ ss.insert(curr); curr = curr->next; } } return false; } };
方法三、反转链表(唯一的问题是破坏了原始链表)
环形链表中,环形会死循环没办法判断边界条件,因此我们我们每遍历一个链表就把后面的指向前面的,这样环形要再次循环时会反方向走,最后他会回到头节点,从而循环结束。
环形初始情况:
倒置后:
没有环形就是普通链表的倒置。
所以最后只要判断倒置后的首节点是不是head节点,是则true,不是则false。
时间复杂度最坏是2n(相当于从最后再来一次),即O(n),空间上只用了三个指针即O(1)。
public class Solution { public boolean hasCycle(ListNode head) { if(head==null)return false; if(head.next==head)return true; if(head.next==null)return false; ListNode p=head.next,q=p.next,x; head.next=null; for(x=head,p.next=x;q!=null;p.next=x,x=p,p=q,q=q.next);//倒置整个链表 if(p==head)return true;//如果回到头节点说明存在环,不是则不存在环 else return false; } }
4. 相交链表
编写一个程序,找到两个单链表相交的起始节点。
在节点 c1 开始相交
方法一: 暴力法
对链表A中的每一个结点 a_i,遍历整个链表 B 并检查链表 B 中是否存在结点和 a_i相同。
复杂度分析
时间复杂度 : O(mn)。
空间复杂度 : O(1)。
方法二、哈希表
遍历链表A并将每个结点的地址/引用存储在哈希表中。然后检查链表B中的每一个结点 b_i是否在哈希表中。若在,则 b_i为相交结点。
复杂度分析
时间复杂度 : O(m+n)。
空间复杂度 : O(m)或O(n)。
class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { if(headA==nullptr || headB==nullptr) return nullptr; ListNode *p = headA; set<ListNode *> ss; while(p){ ss.insert(p); p = p->next; } p = headB; while(p){ if(ss.find(p)!=ss.end()){ //找到相同元素 return p; }else{ p = p->next; } } return nullptr; } };
方法三、双指针法
创建两个指针pA和pB,分别初始化为链表A和B的头结点。然后让它们向后逐结点遍历。
当pA到达链表的尾部时,将它重定位到链表B的头结点 (你没看错,就是链表B); 类似的,当pB到达链表的尾部时,将它重定位到链表A的头结点。
若在某一时刻pA和pB相遇,则pA/pB为相交结点。
想弄清楚为什么这样可行, 可以考虑以下两个链表: A={1,3,5,7,9,11}和B={2,4,9,11},相交于结点9。由于B.length (=4)< A.length(=6),pB比pA少经过2个结点,会先到达尾部。将pB重定向到A的头结点,pA重定向到B的头结点后,pB要比pA多走2个结点。因此,它们会同时到达交点。
如果两个链表存在相交,它们末尾的结点必然相同。因此当pA/pB到达链表结尾时,记录下链表A/B对应的元素。若最后元素不相同,则两个链表不相交。
class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { if(headA==nullptr || headB==nullptr) return nullptr; ListNode *pa = headA; ListNode *pb = headB; /* * 如果是没有相交的链表,最后两个指针将分别指向尾部的nullptr,由相等退出循环 */ while(pa != pb){ pa = (pa==nullptr)?headB:pa->next; pb = (pb==nullptr)?headA:pb->next; } return pa; } };
5. 移除链表元素
删除链表中等于给定值 val 的所有节点。
方法一、新建虚拟头节点
由于可能会设计到头节点的删除,因此新建一个头头节点。
class Solution { public: ListNode* removeElements(ListNode* head, int val) { if(head==nullptr) return nullptr; ListNode* newHead = new ListNode(-1); newHead->next=head; ListNode *pre = newHead, *curr=head; while(curr){ if(curr->val==val){//删除 pre->next=curr->next; curr=curr->next; }else{ pre = curr; curr=curr->next; } } return newHead->next; } };
方法二、递归
class Solution { public ListNode removeElements(ListNode head, int val) { if(head==null) return null; head.next=removeElements(head.next,val); //一直到尾元素,才开始处理下面的语句,即从后往前判断是否由target值。 if(head.val==val){ return head.next; }else{ return head; } } }
方法三、头节点另考虑
class Solution { public ListNode removeElements(ListNode head, int val) { //删除值相同的头结点后,可能新的头结点也值相等,用循环解决 while(head!=null&&head.val==val){ head=head.next; } if(head==null) return head; ListNode prev=head; //确保当前结点后还有结点 while(prev.next!=null){ if(prev.next.val==val){ prev.next=prev.next.next; }else{ prev=prev.next; } } return head; } }
6. 反转链表
方法一、递归
递归版本稍微复杂一些,其关键在于反向工作。假设列表的其余部分已经被反转,现在我该如何反转它前面的部分?
class Solution { public: ListNode* reverseList(ListNode* head) { //用三个相邻的指针从前往后移 if(head==nullptr || head->next==nullptr) return head; ListNode *pre = reverseList(head->next); head->next->next=head; head->next =nullptr; return pre; } };
方法二、迭代
头插法:新建虚拟头节点
class Solution {
public:
ListNode* reverseList(ListNode* head) { //头插法
if(head==nullptr)
return head;
ListNode* newHead = new ListNode(-1);
newHead->next = head;
ListNode* pre=head;
ListNode* curr=pre->next;
while(curr){
pre->next = curr->next;
curr->next = newHead->next; //每一次插入到头元素,此处不是pre
newHead->next = curr;
curr = pre->next; //指向下一个要插入的节点
}
return newHead->next;
}
};
用三个相邻的指针从前往后移
class Solution { public: ListNode* reverseList(ListNode* head) { //用三个相邻的指针从前往后移 if(head==nullptr) return head; ListNode *pre = nullptr, *curr=head, *pnext=head->next; while(pnext){ curr->next=pre; pre = curr; curr=pnext; pnext=pnext->next; } curr->next=pre; return curr; } };
7. 回文链表
能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题
方法一、快慢指针+栈
bool isPalindrome(ListNode* head) { if(head == nullptr) return true; if(head->next == nullptr) return true; stack<int> s; ListNode* slow; ListNode* fast; int flag = 0; // 使用快慢指针遍历前半部分的链表, 并使用stack保存值 slow = fast = head; while(fast){ s.push(slow->val); slow = slow->next; if(fast->next != nullptr) fast = fast->next->next; else{ fast = nullptr; flag = 1; } } // 判断如果链表是奇数个, 就删除栈顶保存的链表中间的值 if(flag){ s.pop(); } // 遍历后半部分的值,并与stack栈顶的值做比较, 不同则不是回文链表 while(slow){ if(slow->val != s.top()){ return false; } s.pop(); slow = slow->next; } return true; }
方法二、快慢指针确定中部位置,反转前半部分链表,然后比较反转后的两个 半链表
快慢指针适合用于有中点相关的场景,同时为了更加简便,我们可以采用一边移动一边将其倒置,这个倒置的顺序只需要在图上画一画就凑出来了,同时需要注意链表个数为奇数和为偶数对于后一半的头结点位置有所不同。
class Solution { public: bool isPalindrome(ListNode* head) { if(!head||!head->next) return true; ListNode*pre = NULL; ListNode*slow = head; ListNode*fast = head; ListNode*s = NULL; while(fast != NULL && fast->next != NULL) { pre = slow; slow = slow->next; fast = fast->next->next; pre->next = s; s = pre; } ListNode*temp = slow; if(fast != NULL) temp = temp->next; slow = pre; while(temp!=NULL) if(temp->val!=slow->val) return false; else {temp = temp->next;slow = slow->next;} return true; } };
8. 删除链表中的节点
只给定要删除的节点,并不知道链表的头节点,也不知道当前节点的头节点,怎么删除当前节点。
思路:将下一个节点的值赋给当前节点,然后将当前节点当作前节点,删除下一个节点即可。
class Solution { public: void deleteNode(ListNode* node) { node->val = node->next->val; node->next = node->next->next; return; } };
9. 链表的中间结点
方法一、使用双指针,一个每次走一步p1、一个每次走两步p2
奇数时,当p2走到最后一个节点,p1走到中间。(p2->next为空)
偶数时,当p2走到最后一个节点的下一个节点(null),p1走到中间的第二个节点处。(p2为空)
因此,中止条件是,p2为空或者p2->next为空。
class Solution { public: ListNode* middleNode(ListNode* head) { if(head==nullptr) return head; ListNode *p1=head, *p2=head; while(p2 && p2->next){ p1=p1->next; p2=p2->next->next; } return p1; } };
方法二、按顺序将每个结点放入数组 A
中。然后中间结点就是 A[A.Length/2]
,因为可以通过索引检索每个结点。