【力扣探索】链表——经典问题

写在前面的提示

  1. 通过一些测试用例可以节省您的时间
    使用链表时不易调试。因此,在编写代码之前,自己尝试几个不同的示例来验证您的算法总是很有用的。
  2. 你可以同时使用多个指针
    有时,当你为链表问题设计算法时,可能需要同时跟踪多个结点。您应该记住需要跟踪哪些结点,并且可以自由地使用几个不同的结点指针来同时跟踪这些结点。如果你使用多个指针,最好为它们指定适当的名称,以防将来必须调试或检查代码。
  3. 在许多情况下,你需要跟踪当前结点的前一个结点
    你无法追溯单链表中的前一个结点。因此,您不仅要存储当前结点,还要存储前一个结点。

练习

1. 反转链表

题目链接: 206.反转链表
解题思路

  1. 迭代,时间复杂度O(n),空间复杂度O(1)
  2. 递归,时间复杂度O(n),空间复杂度O(n),由于使用递归,将会使用隐式栈空间。递归深度可能会达到 n 层
  3. 双指针,时间复杂度O(n),空间复杂度O(1)

建议食用题解中某大佬的思路展示,图文结合非常清晰here

代码

//迭代版本,时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
    	if(head == NULL || head->next == NULL) return head;

    	ListNode *newhead = head;
    	while(head->next != NULL){
    		ListNode *p = head->next;
    		head->next = p->next;
    		p->next = newhead;
    		newhead = p;
    	}
    	return newhead;
    }
};

//递归版本,时间复杂度O(n),空间复杂度O(n)
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
    	if(head == NULL || head->next == NULL) return head;
    	ListNode *newhead = reverseList(head->next);
    	head->next->next = head;
    	head->next = NULL;
    	return newhead;
    }
};

2. 移除链表元素

题目链接: 203. 移除链表元素
解题思路:题目较简单,dummyhead的使用使得讨论的case大大减少。

  1. 迭代结合虚拟头节点,注意循环中当next需要被删除时,当前遍历到的r指针不能前进,否则会丢失处理对象。时间复杂度O(n),空间复杂度O(1)
  2. 递归实现,假设当前节点后面都已经处理好,转而判断当前节点val是否为目标val,进而返回对应结果

代码

//递归版本
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
    	if(head == NULL) return head;
    	ListNode *ret = removeElements(head->next, val);
    	if(head->val == val) return ret;
    	head->next = ret;
    	return head;
    }
};

//迭代+虚拟头节点
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
    	if(head == NULL) return head;
    	ListNode *dummyhead = new ListNode(0);
    	dummyhead->next = head;

    	ListNode* r = dummyhead;
    	while(r->next != NULL){
    		if(r->next->val == val){
    			r->next = r->next->next;
    		}else{//此时若下一个节点需要删除,那么r应该保留位置,而不是前进
    			r = r->next;
    		}
    	}

    	return dummyhead->next;
    }
};

3. 奇偶链表

题目链接: 328. 奇偶链表
解题思路

  1. 奇偶链表先拆分,后合并,思路简单清晰,到末尾的处理很关键,涉及while循环的终止条件,自己编程的结果明显想的复杂,虽通过,但不如题解中给出的简洁明了。

代码

class Solution {
public:
    ListNode* oddEvenList(ListNode* head) {
        if(head == NULL || head->next == NULL || head->next->next == NULL) return head;

        ListNode *oddhead = head, *oddp = head;
        ListNode *evenhead = head->next, *evenp = head->next;

        while(oddp->next != NULL && evenp->next != NULL){
        	if(oddp->next != NULL && oddp->next->next != NULL){
        		oddp->next = oddp->next->next;
        		oddp = oddp->next;
        	}
        	else if(oddp->next == NULL || oddp->next->next == NULL) oddp->next = NULL;

        	if(evenp->next != NULL && evenp->next->next != NULL){
        		evenp->next = evenp->next->next;
        		evenp = evenp->next;
        	}
        	else if(evenp->next == NULL || evenp->next->next == NULL) evenp->next = NULL;
        }

        oddp->next = evenhead;
        return oddhead;
    }
};
//官方题解
class Solution {
public:
    ListNode* oddEvenList(ListNode* head) {
        if(head == NULL || head->next == NULL || head->next->next == NULL) return head;

        ListNode *oddhead = head, *oddp = head;
        ListNode *evenhead = head->next, *evenp = head->next;

        while(evenp != NULL && evenp->next != NULL){
        	oddp->next = evenp->next;
        	oddp = oddp->next;
        	evenp->next = oddp->next;
        	evenp = evenp->next;
        }
        oddp->next = evenhead;	
        return oddhead;
    }
};		

4. 回文链表

题目链接: 234. 回文链表
解题思路:说几个比较推荐的解题思路

  1. 构造数组判断,由于链表的特性对元素的访问十分消耗时间,通过构建对应的数组,可以进一步使用双指针从两端逐一比对。缺点是空间复杂度达到了O(n)
  2. 在原数组上利用快慢指针,将链表一分为二,反转其中的一部分,再与另一部分进行比较,若完全相同则是回文链表,利用该方法可以实现O(1)的额外空间复杂度,另外要注意应恢复链表的结构,由于此题对此不要求,我的代码中没有恢复链表的结构。该题中关于快慢指针和反转的实现细节,题解中的大神提出了不同的方案:
    a. 传统方案,快慢指针和反转分为独立的两个部分,通过慢指针确定反转的位置, 这里反转的是后半部分的链表,然后再进行反转操作,最后与前半部分链表比较。
    b. 加鸡腿方案, 快慢指针的遍历和反转操作融合在一起,使用一个循环即可, 这里反转的是链表的前半部分, 当慢指针遍历的同时对前半部分进行反转操作, 慢指针遍历结束后的位置可确定后半部分链表的开始位置, 再比较即可.
  3. 哈希公式, 似乎利用公式对回文的处理非常快,码住会了回来补充!
  4. 暴力递归+暴力迭代 (逐对定位比较,很蠢)

代码

//快慢指针反转后半部分链表进行比较,
class Solution {
public:
    bool isPalindrome(ListNode* head) {
    	if(head == NULL || head->next == NULL) return true;
    	ListNode *slow = head, *fast = head;
    	while(fast != NULL && fast->next != NULL && fast->next->next != NULL){
    		fast = fast->next->next;
    		slow =slow->next;
    	}
    	ListNode *cur = slow->next, *pre = NULL;
    	ListNode *save = cur->next;
    	while(save != NULL){
    		cur->next = pre;
    		pre = cur;
    		cur = save;
    		save = save->next;
    	}
    	cur->next = pre;    	
    	ListNode *p = cur;
    	ListNode *q = head;
    	while(p != NULL){
    		if(q->val != p->val) return false;
    		p = p->next;
    		q = q->next;
    	}
    	return true;
    }
};

//快慢指针遍历翻转一遍过,
class Solution {
public:
    bool isPalindrome(ListNode* head) {
    	if(head == NULL || head->next == NULL) return true;
    	if(head->next->next == NULL) return head->val == head->next->val;
    	ListNode *slow = head, *fast = head;
    	ListNode *pre = NULL, *save = head->next;
    	while(fast != NULL && fast->next != NULL && fast->next->next != NULL){
    		fast = fast->next->next;
    		slow->next = pre;
    		pre = slow;
    		slow = save;
    		save = save->next;
    	}
    	slow->next = pre;
    	if(fast->next == NULL) slow = slow->next;
    	while(save != NULL){
    		if(save->val != slow->val) return false;
    		save = save->next;
    		slow = slow->next;
    	}
    	return true;
    }
};

5. 合并两个有序链表

题目链接: 21. 合并两个有序链表
解题思路
经典迭代的想法,两个指针从两个链表各自出发分别比较,每次迭代中符合条件的插入到新链表后面,时间复杂度O(m+n),空间复杂度O(1)
代码

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode *p1 = l1, *p2 = l2;
        ListNode* resdummyhead = new ListNode(-1);
        ListNode* restail = resdummyhead;
        while(p1 != NULL && p2 != NULL){
        	if(p1->val < p2->val){
        		restail->next = p1;
        		restail = p1;
        		p1 = p1->next; 
        	}
        	else{
        		restail->next = p2;
        		restail = p2;
        		p2 = p2->next;
        	}
        }
        if(p1 == NULL) restail->next = p2;
        if(p2 == NULL) restail->next = p1;
        return resdummyhead->next;
    }
};

6. 两数相加

题目链接: 2. 两数相加
解题思路

  1. 考虑到两个链表的低位在表头,所以可以依次从头开始对节点进行相加,同时注意用变量存储进位的情况,在两者都遍历完之后,若仍有进位,应该在结尾再补充一个节点。时间/空间复杂度O(max(m,n))
  2. 我尝试过用数值直接进行计算再分解的操作,这对于一些长度较小的链表是可行的,但是当树木变大,传统的int,long等类型无法存储那么大的数会导致算法崩溃。
    代码
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *p1 = l1;
        ListNode *p2 = l2;
        ListNode *resdummyhead = new ListNode(-1);
        ListNode *resp = resdummyhead;
        int count = 0;
        while(p1 != nullptr || p2 != nullptr){
        	int addres;
        	if(p1 == nullptr) addres = p2->val + count;
        	else if(p2 == nullptr) addres = p1->val + count;
        	else addres = p1->val + p2->val + count;
        	count  = 0;
        	if(addres >= 10){
        		count = 1;
        		addres = addres - 10;
        	}
        	ListNode *r = new ListNode(addres);
        	resp->next = r;
        	resp = r;
        	if(p1) p1 = p1->next;
        	if(p2) p2 = p2->next;
        }
        if(count == 1){
        	ListNode *r = new ListNode(1);
        	resp->next = r;     		
        }
        p1 = resdummyhead->next;
        delete resdummyhead;
        return p1;
    }
};

7. 扁平化多级双向链表

题目链接: 430. 扁平化多级双向链表
解题思路

  1. 递归解决,很容易想到,每次检测到某节点child不为null,假设指向的链表已经flaten,然后进行操作。由于用了递归其消耗的空间较大
  2. 利用while循环迭代解决,代码清晰,思路非常巧妙,能够成功的原因是每次p处理完了相应的child链表后,往前进的时候其实是会先遍历完原来的子链表,这样实际上就完成了一个深度遍历,推荐!
    代码
//递归版本
class Solution {
public:
    Node* flatten(Node* head) {
    	if(head == NULL) return NULL;
        Node *p = head;
        while(p != NULL){
        	if(p->child != NULL){
        		Node *temp = flatten(p->child);
        		Node *r = temp;
        		while(r->next != NULL) r = r->next;
        		p->child = NULL;
        		temp->prev = p;
        		r->next = p->next;
        		p->next = temp;
        		if(r->next != NULL)
        			r->next->prev = r;
        		p = r->next;
        	}
        	else p = p->next;
        }
        return head;
    }
};
//迭代版本,推荐!
class Solution {
public:
    Node* flatten(Node* head) {
        Node *p = head;
        while(p != NULL){
        	if(p->child != NULL){
        		Node *child = p->child;
        		Node *next = p->next;
        		p->child =NULL;
        		child->prev = p;
        		p->next = child;      
        		while(child->next != NULL) child = child->next;
        		child->next = next;
        		if(next != NULL)
        			next->prev = child;
        	}
        	p = p->next;
        }
        return head;
    }
};

8.

题目链接: 138. 复制带随机指针的链表
解题思路

  1. 遍历原链表复制每个节点在对应原节点的右边(next指针指向),生成一个2*N的链表,遍历的时候对于random不为空的节点可以依靠相对位置确定其右边节点的random指针指向的位置。此方法只用到O(1)的额外空间复杂度。
  2. 递归迭代解放见官方题解,第一种思路是其中第三种解法,前两种方法难懂且最后额外的空间复杂度为O(n)。
    代码
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head == NULL) return NULL;
        Node *p = head;
        while(p != NULL){
            Node *node = new Node(p->val);
            node->next = p->next;
            p->next = node;           
            p = p->next->next;
        } 
        p = head;
        while(p != NULL){
            if(p->random != NULL) p->next->random = p->random->next;
            p = p->next->next;
        }
        p = head;
        Node *newhead = head->next;
        Node *np = newhead;
        while(p != NULL){
            p->next = np->next;
            p = p->next;
            if(p != NULL){ 
                np->next = p->next;
                np = np->next;
            }         
        }
        return newhead;
    }
};

9. 旋转链表

题目链接: 61. 旋转链表
解题思路

  1. 注意到旋转次数到达链表长度的时候i相当于没有旋转,因此实际旋转的效果=k%length的结果,可以将原链表的尾部next链接到原链表head,由k与length计算出应该切断的地方,找到即可。时间复杂度O(n),空间复杂度O(1)

代码

class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
    	if(head == nullptr || head->next == nullptr) return head; 
    	int length = 1;
    	ListNode *p = head;
    	while(p->next != nullptr){
    		p = p->next;
    		length++;
    	}
    	p->next = head;
    	k = k % length;
    	p = head;
    	for(int i = 0; i < length-k-1; ++i){
    		p = p->next;
    	}
    	head = p->next;
    	p->next = nullptr;
    	return head;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值