206、反转链表
(1)双指针法(迭代)
//自己的思路,该方法无法处理空链表的情况
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode*cur, *pre;
cur = head;
pre = head->next; //若为空链表则head为空,此句报错。
cur->next = NULL;
while(pre!=NULL){
ListNode* t = pre->next;
pre->next = cur;
cur = pre;
pre = t;
}
return cur;
}
};
//官方解答
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode*cur, *pre;
cur = NULL;
pre = head;
while(pre!=NULL){
ListNode* t = pre->next;
pre->next = cur;
cur = pre;
pre = t;
}
return cur;
}
};
(2)递归法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==NULL||head->next==NULL)
return head;
ListNode* result = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return result;
}
};
使用递归函数,一直递归到链表的最后一个结点,该结点就是反转后的头结点,记作 ret 。此后,每次函数在返回的过程中,让当前结点的下一个结点的 next 指针指向当前节点。同时让当前结点的 next 指针指向 NULL,从而实现从链表尾部开始的局部反转。当递归函数全部出栈后,链表反转完成。
21、合并两个有序链表
(1)迭代法(双指针法)
添加哑结点简化代码,当一方为空节点时结束迭代,把非空一方的剩余节点加入答案中。
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* preHead = new ListNode();
ListNode* pre = preHead;
while(l1!=NULL && l2!=NULL){
if(l1->val<=l2->val){
pre->next = l1;
l1 = l1->next;
}
else{
pre->next = l2;
l2 = l2->next;
}
pre = pre->next;
}
pre->next = (l1==NULL) ? l2 : l1;
return preHead->next;
}
};
(2)递归法
比较两个链表的头节点,递归调用函数,注意终止条件。
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1==nullptr){
return l2;
}
else if(l2==nullptr){
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;
}
}
};
19、删除链表中的倒数第N个节点
(1)计算链表长度
技巧:在对链表进行操作时,一种常用的技巧是添加一个哑节点(dummy node),它的next 指针指向链表的头节点。这样一来,我们就不需要对头节点进行特殊的判断了。结果返回哑结点的next(即原链表的头结点)。
class Solution {
public:
int getLength(ListNode* head){
int Length = 0;
while(head){
++Length;
head = head->next;
}
return Length;
}
ListNode* removeNthFromEnd(ListNode* head, int n) {
int length = getLength(head);
//声明哑节点,不需要对头结点进行特殊判断
ListNode* dummy = new ListNode(0, head);
ListNode* curNode = dummy;
for(int i=1; i<length-n+1; i++){
curNode = curNode->next;
}
curNode->next = curNode->next->next;
//声明另一个节点用来应对删除的是头结点的情况
//(若删除头结点且返回head的话,则返回的只是第一个节点,不是链表)
ListNode* res = dummy->next;
delete dummy;
return res;
}
};
(2)双指针法(快慢指针)
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* first = dummy; //快指针
ListNode* second = dummy; //慢指针
int index =0;
//快慢指针相差n个位置
while(index<n){
first = first->next;
index++;
}
//快指针到终点,慢指针到达被删除节点的前置节点
while(first->next){
first = first->next;
second = second->next;
}
second->next = second->next->next;
ListNode* res = dummy->next;
delete dummy;
return res;
}
};
(3)栈
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* cur = dummy;
stack<ListNode*> stk;
while(cur){
stk.push(cur);
cur = cur->next;
}
for(int i=0; i<n; i++){
stk.pop();
}
ListNode* target = stk.top();
target->next = target->next->next;
ListNode* res = dummy->next;
delete dummy;
return res;
}
};
106、相交链表
(1)双指针
//自己写的,时间复杂度O(m+n),空间复杂度O(1)
class Solution {
public:
int getlength(ListNode* head){
int length = 0;
while(head){
length++;
head = head->next;
}
return length;
}
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lengthA = getlength(headA);
int lengthB = getlength(headB);
ListNode* pA(headA), *pB(headB);
if(lengthA>lengthB){
for(int i=0; i<lengthA-lengthB; i++)
pA = pA->next;
}
else{
for(int i=0; i<lengthB-lengthA; i++)
pB = pB->next;
}
do{
if(pA==pB) return pA;
pA = pA->next;
pB = pB->next;
}while(pA);
return NULL;
}
};
//官方思路路人解法
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(!headA || !headB)
return nullptr;
ListNode* pA(headA), *pB(headB);
while(pA!=pB){
//当两个链表长度相同时,两指针最终都会指向null,从而跳出循环
pA = (pA? pA->next: headB);
pB = (pB? pB->next: headA);
}
return pA;
}
};
(2)哈希表法
//官方思路,路人解法
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
set<ListNode*> mSet; //使用set,时间复杂度比unordered_set长
ListNode* pA = headA;
ListNode* pB = headB;
while(pA){
mSet.insert(pA);
pA = pA->next;
}
while(pB){
if(mSet.find(pB)!=mSet.end())
return pB;
pB = pB->next;
}
return nullptr;
}
};
141、环形链表
(1)hash表法
//自己的思路
class Solution {
public:
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> list;
ListNode* curNode = head;
while(curNode){
if(list.count(curNode)>0)
return true;
list.insert(curNode);
curNode = curNode->next;
}
return false;
}
};
(2)双指针法(龟兔赛跑)
//官方解答:快慢指针,只要有环,快慢指针一定会重合。
//快指针从第二位开始是因为若都从head开始的话进不去while循环。
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==nullptr || head->next==nullptr)
return false;
ListNode* fast(head->next), *slow(head);
while(fast!=slow){
if(fast==nullptr || fast->next==nullptr)//若fast->next==nullptr判断放在前,是否会判断出错?
return false;
fast = fast->next->next;
slow = slow->next;
}
return true;
}
};
142、环形链表II
(1)哈希表法
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
unordered_set<ListNode*> mSet;
ListNode* cur = head;
while(cur){
if(mSet.count(cur))
return cur;
mSet.insert(cur);
cur = cur->next;
}
return NULL;
}
};
(2)双指针法
先用快慢指针判断是否为环形链表(思路同141.环形链表,但快慢指针同时从头节点出发),慢指针一次前进1步快指针一次前进2步,若有环则必在环内相遇,设链表头部到环入口的距离为a,慢指针在环内走过的距离为b,而快指针在环内已经走过n圈,则快指针走过的距离为a+n(b+c)+b,且快指针走过的距离始终为慢指针的2倍,则有a+n(b+c)+b=2(a+b),即a=c+(n-1)(b+c)。因此,当快慢指针相遇时,再使用额外的指针ptr指向头节点,ptr与慢指针同时出发,当慢指针走过n-1圈外加c的距离后,与ptr在入环处相遇。
中间加入各种边界判断,如空链表、单节点链表、非环形链表等。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast(head), *slow(head);
while(fast!=nullptr){
slow = slow->next;
if(fast->next==nullptr)
return NULL;
fast = fast->next->next;
if(slow==fast){
ListNode* ptr = head;
while(ptr!=slow){
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return NULL;
}
};
2、两数相加
(1)暴力解法
自己的思路,因为两个链表非空,所以用两个指针分别从两个链表的头节点同时出发,将对应的val相加存在任意链表(此处为l1)的相应位置,注意考虑进位的情况。当有链表到达末尾时,用从哑节点出发的指针cur来连接还未到达末尾的链表剩余的部分,同样别忘记考虑进位的情况。待两个链表都到达末尾时,判断最后一个节点的val是否有进位,若有还需new一个节点,此时用到另一个指针end(比cur落后一位)。
该方法过于冗杂,考虑情况较为杂乱。时间复杂度为O(max(m,n)),空间复杂度为O(1)。
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
//if(l1->val==0) return l2;
//else if(l2->val==0) return l1;
ListNode* dummy = new ListNode(0,l1);
ListNode* cur = dummy;
int ext = 0;
while(l1!=nullptr && l2!=nullptr){
l1->val = l1->val + l2->val + ext;
ext = l1->val>9 ? 1 : 0;
l1->val = l1->val%10;
l1 = l1->next;
l2 = l2->next;
cur = cur->next;
}
ListNode* end = cur;
//还得判断剩余链表节点的val+ext是否大于9
if(l1==nullptr){
cur->next = l2;
cur = cur->next;
}
else{
cur->next = l1;
cur = cur->next;
}
while(cur!=nullptr){
cur->val = cur->val + ext;
ext = cur->val>9 ? 1 : 0;
cur->val = cur->val%10;
cur = cur->next;
end = end->next; //end始终比cur慢一位
}
//判断最后一位是否大于9
if(ext>0){
ListNode* endNode = new ListNode(1);
end->next = endNode; //在链表末尾添加元素必须使用最后一个元素
}
ListNode* res = dummy->next;
delete dummy;
return res;
}
};
(2)官方解法
由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1、n2,进位值为carry,则它们的和为 n1+n2+carry;其中,答案链表处相应位置的数字为 (n1+n2+carry)%10,而新的进位值为 1。如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。此外,如果链表遍历结束后,有carry>0,还需要在答案链表的后面附加一个节点,节点的值为 1。
该方法将两个链表长度相同和长度不同的情况统一了起来,且答案链表中的每个节点都是new出来的(学到了)。时间复杂度O(max(m,n)),空间复杂度为O(max(m,n))。
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* head(nullptr), *tail(nullptr);
int carry = 0;
while(l1!=nullptr || l2!=nullptr){
int n1 = l1!=nullptr ? l1->val : 0;
int n2 = l2!=nullptr ? l2->val : 0;
int sum = n1 + n2 + carry;
if(head==nullptr)
head = tail = new ListNode(sum%10);
else{
tail->next = new ListNode(sum%10);
tail = tail->next;
}
carry = sum>9 ? 1 : 0;
if(l1!=nullptr) l1 = l1->next;
if(l2!=nullptr) l2 = l2->next;
}
if(carry>0)
tail->next = new ListNode(1);
return head;
}
};
234、回文链表
(1)栈
自己想的,先将链表的元素依次存入栈中,再用cur指向链表头。每次从栈顶取出一个节点元素同时cur指针前进一位,判断从栈中取出节点的val和cur指向节点的val是否相等,若不等则为非回文链表(注意空链表也是回文链表,题中未指出)。
该方法时间复杂度为O(n),空间复杂度为O(n)。
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head==nullptr)
return true;
stack<ListNode*> mStack;
for(auto p1=head; p1!=nullptr; p1=p1->next)
mStack.push(p1);
ListNode* cur = head;
while(cur!=nullptr){
auto p2 = mStack.top();
if(p2->val!=cur->val)
return false;
mStack.pop();
cur = cur->next;
}
return true;
}
};
(2)双指针法
官方解答,
整个流程可以分为以下五个步骤:
- 找到前半部分链表的尾节点。
- 反转后半部分链表。
- 判断是否回文。
- 恢复链表。
- 返回结果。
执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。
我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。若链表有奇数个节点,则中间的节点应该看作是前半部分。
步骤二可以使用「206. 反转链表」问题中的解决方法来反转链表的后半部分。
步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。
步骤四与步骤二使用的函数相同,再反转一次恢复链表本身。
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head==nullptr)
return true;
ListNode* firstHalf = endOfFirstHalf(head);
ListNode* secondHalf = reverseList(firstHalf->next);
ListNode* p1(head), *p2(secondHalf);
bool result = true;
while(result && p2!=nullptr){
if(p1->val!=p2->val)
result = false;
p1 = p1->next;
p2 = p2->next;
}
firstHalf->next = reverseList(secondHalf);
return result;
}
ListNode* endOfFirstHalf(ListNode* head){
ListNode* fast(head), *slow(head);
while(fast->next!=nullptr && fast->next->next!=nullptr){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
ListNode* reverseList(ListNode* head){
ListNode* pre(nullptr), *cur(head);
while(cur!=nullptr){
ListNode* next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
};