链表划分、多指针节点、链表合并类题目的常规思路—Leetcode-thinking_record03

目录

链表划分(Medium)

大体思路

细节设计

代码实现

经验总结 

复杂的链表的深度拷贝(Hard)

程序结构搭建

大体思路

细节设计

代码实现

经验总结  

排序链表的合并(2个)(Easy)

大体思路 

细节设计

 代码实现

排序链表的合并(多个)(Hard)

Solve1:暴力合并(超时)

大体思路 

细节设计

Solve2:排序后相连(可解,非最优)

大体思路 

细节设计

代码实现

Solve3:分治后相连(最优)

大体思路 

细节设计

 代码实现

经验总结


链表划分(Medium)

LeetCode 86.Partition List

已知链表头指针head与数值下,将所有小于x的节点放在大于或等于x的节点前,且保持这些节点的原来的相对位置。

大体思路

巧用临时的头节点,设置两个临时节点, less_hand和more_hand,这两个临时节点,遍历链表的每个节点,然后比比较节点x小的,插入到less_hand指向节点的后面,比比较节点x大的,插入到more_hand指向节点的后面,最后,再把它们连起来。

细节设计

Step1:对每个节点顺序遍历进行判断,并根据与比较节点的比对,分配到不同的临时节点后面:

Step2:将重新分配好的两个子链表进行连接,把临时节点more_head去掉:

Step3:返回不带临时节点less_hand的头节点,完成划分

代码实现


#include <stdio.h>
	
struct ListNode {
	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
    	ListNode less_head(0);
    	ListNode more_head(0);
    	ListNode *less_ptr = &less_head;
    	ListNode *more_ptr = &more_head;
        while(head){
        	if (head->val < x){
        		less_ptr->next = head;
        		less_ptr = head;
			}
			else {
				more_ptr->next = head;
				more_ptr = head;
			}
        	head = head->next;
        }
        less_ptr->next = more_head.next;
        more_ptr->next = NULL;
        return less_head.next;
    }
};

int main(){
	ListNode a(1);
	ListNode b(4);
	ListNode c(3);
	ListNode d(2);
	ListNode e(5);
	ListNode f(2);
	a.next = &b;
	b.next = &c;
	c.next = &d;
	d.next = &e;
	e.next = &f;	
	Solution solve;
	ListNode *head = solve.partition(&a, 3);	
	while(head){
		printf("%d\n", head->val);
		head = head->next;
	}
	return 0;
}

经验总结 

  • 遇到链表划分类的题目时,要考虑抓准比较节点,以比较节点为依据,来派分链表中每个节点的位置。

复杂的链表的深度拷贝(Hard)

LeetCode 138.Copy List with Random Pointer

已知一个复杂的链表,节点中有一个指向本链表任意某个节点的随机指针(也可以为空),求这个链表的深度拷贝。

程序结构搭建

大体思路

本题的难点在于,将random指针,指向的逻辑关系,在新链表中给生成出来。就比如说,我们可以容易的实现创建一个新的链表并按照原链表的顺序将其相连,但是,如何将random指针重新根据原来的顺序进行复制,这才是这道题的难点。

我们可以根据节点地址与节点序号对应的思路来设计,我们完成了将新链表按原链表的连接之后,我们处理每个random指针的时候,实际是需要做两个操作的,第一步是需要知道在原始链表中的对应节点的random指针到底指向的那里,第二步就是将上一步获得的逻辑关系给复制到新链表的对应节点上,并且还得知道其所指向的地址是什么,即需要知道新链表中的任意一个节点对应的是第几号节点。

Map优势就在于,可以将当前节点的节点序号和random指针指向的节点序号及地址绑定在一起,这样就方便直接找到建立联系。

细节设计

通过map来获得映射关系,

Step1:按照传统的链表复制,将新链表先搭建起来,之后再去填每个节点的random的指针。

Step2:先知道在原链表中当前节点的random指针指向的是第几个节点

Step3:再在新链表中将当前节点的random还指向在原链表中指向的节点。

代码实现逻辑:

代码实现

	
#include <stdio.h>
		
struct RandomListNode {
	int label;
	RandomListNode *next, *random;
	RandomListNode(int x) : label(x), next(NULL), random(NULL) {}
};

#include <map>
#include <vector>

class Solution {
public:
    RandomListNode *copyRandomList(RandomListNode *head) {
    	std::map<RandomListNode *, int> node_map;
    	std::vector<RandomListNode *> node_vec;
    	RandomListNode *ptr = head;
    	int i = 0;
    	while (ptr){
	    	node_vec.push_back(new RandomListNode(ptr->label));
	    	node_map[ptr] = i;
	    	ptr = ptr->next;
	    	i++;
	    }
	    node_vec.push_back(0);
	    ptr = head;
	    i = 0;
	    while(ptr){
    		node_vec[i]->next = node_vec[i+1];
    		if (ptr->random){
    			int id = node_map[ptr->random];
		    	node_vec[i]->random = node_vec[id];
		    }
    		ptr = ptr->next;
    		i++;
    	}
    	return node_vec[0];
    }
};

int main(){
	RandomListNode a(1);
	RandomListNode b(2);
	RandomListNode c(3);
	RandomListNode d(4);
	RandomListNode e(5);
	a.next = &b;
	b.next = &c;
	c.next = &d;
	d.next = &e;	
	a.random = &c;
	b.random = &d;
	c.random = &c;
	e.random = &d;	
	Solution solve;
	RandomListNode *head = solve.copyRandomList(&a);	
	while(head){
		printf("label = %d ", head->label);
		if (head->random){
			printf("rand = %d\n", head->random->label);
		}
		else{
			printf("rand = NULL\n");
		}
		head = head->next;
	}
	return 0;
}

经验总结  

  • 当遇到每个节点所携带的指针超过一个的时候,要考虑借用map映射的方式进行绑定。

排序链表的合并(2个)(Easy)

LeetCode 21.Merge Two Sorted Lists

已知两个已排序链表头节点指针l1和l2,将这两个链表合并,合并后仍为有序的,返回合并后的头节点。(默认l1和l2两个链表的每个节点内的值是顺序递增排列的)

大体思路 

比较l1和l2指向的节点,将较小的节点插入到pre指针后,并向后移动较小节点对应的指针。

细节设计

Step1:设置新链表头节点pre,并同时从l1和l2的各自头节点开始遍历。

Step2:如果l1和l2当前的节点有大小区分,就把大的给接到pre后面,再把小的那个给接着接到大的后面,如果这俩相等,就只接一个。

Step3:接着遍历l1和l2的每个节点,并都执行step2,直到两个链表都遍历完。

 代码实现

#include <stdio.h>

struct ListNode {
 	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    	ListNode temp_head(0);
    	ListNode *pre = &temp_head;
    	while (l1 && l2){
	    	if (l1->val < l2->val){
	    		pre->next = l1;
	    		l1 = l1->next;
	    	}
	    	else{
	    		pre->next = l2;
	    		l2 = l2->next;
	    	}
	    	pre = pre->next;
	    }
	    if (l1){
    		pre->next = l1;
    	}
    	if (l2){
	    	pre->next = l2;
	    }
        return temp_head.next;
    }
};

int main(){
	ListNode a(1);
	ListNode b(4);
	ListNode c(6);
	ListNode d(0);
	ListNode e(5);
	ListNode f(7);
	a.next = &b;
	b.next = &c;
	d.next = &e;
	e.next = &f;
	Solution solve;
	ListNode *head = solve.mergeTwoLists(&a, &d);
	while(head){
		printf("%d\n", head->val);
		head = head->next;
	}
	return 0;
}

排序链表的合并(多个)(Hard)

LeetCode 23.Merge k Sorted Lists

已知k个已排序链表头节点指针,将这k个链表合并,合并后仍为有序的,返回合并后的头节点。

Solve1:暴力合并(超时)

大体思路 

最普通的方法,k个链表按顺序合并k-1次。设有k个链表,平均每个链表有n个节点,时间复杂度:

(n+n)+(2n+n)+((k+1)n+n)=(1+2+...+k)n+(k-1)n=(1+2+...+k)n-n=(k^2+k-1)/2*n=O(k^2*n)

这种方法的最大弊端是时间复杂度开销太大!

细节设计

第一个链表和第二个来链表合并,再将这个合并好的链表(由l1和l2合并而成)和第三个链表合并,再将这个合并好的链表(由l1、l2、l3合并而成的)和第四个链表合并,。。。,依次类推。

时间复杂度太高! leetcode上提交的话,只会超时,无法通过。

Solve2:排序后相连(可解,非最优)

大体思路 

将k*n个(所有链表的所有节点)节点放到vector中,再将vector排序(直接用STL里面的内置算法就给解决了),再将节点顺序相连。设有k个链表,平均每个链表有n个节点,时间复杂度:

kN*logkN + kN = O(kN*logkN) (比如k=100,n=10000得到logkN = 20, k = 100)

这种方法也不是最优的。

细节设计

代码实现

#include <stdio.h>

struct ListNode {
 	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};

#include <vector>
#include <algorithm>

bool cmp(const ListNode *a, const ListNode *b){
	return a->val < b->val;
}

class Solution {
public:
    ListNode* mergeKLists(std::vector<ListNode*>& lists) {
        std::vector<ListNode *> node_vec;        
        for (int i = 0; i < lists.size(); i++){
        	ListNode *head = lists[i];
        	while(head){
        		node_vec.push_back(head);
	        	head = head->next;
	        }
        }
        if (node_vec.size() == 0){
        	return NULL;
        }        
        std::sort(node_vec.begin(), node_vec.end(), cmp);
        for (int i = 1; i < node_vec.size(); i++){
        	node_vec[i-1]->next = node_vec[i];
        }
        node_vec[node_vec.size()-1]->next = NULL;
        return node_vec[0];
    }
};

int main(){
	ListNode a(1);
	ListNode b(4);
	ListNode c(6);
	ListNode d(0);
	ListNode e(5);
	ListNode f(7);
	ListNode g(2);
	ListNode h(3);
	a.next = &b;
	b.next = &c;	
	d.next = &e;
	e.next = &f;	
	g.next = &h;	
	Solution solve;	
	std::vector<ListNode *> lists;
	lists.push_back(&a);
	lists.push_back(&d);
	lists.push_back(&g);	
	ListNode *head = solve.mergeKLists(lists);
	while(head){
		printf("%d\n", head->val);
		head = head->next;
	}
	return 0;
}

Solve3:分治后相连(最优)

大体思路 

和之前的思路不一样,我们是先把短的链表进行一个连接,假设有四个短链表,那么就把第一个和第二个合并成一个长链表,再把第三个和第四个合并成一个长的,最后,把这两个长的再合并成一个最长的,这样计算下来,时间复杂度最小。

细节设计

对k个链表进行分制,两两进行合并。设有k个链表,平均每个链表有n个节点,时间复杂度:

第一轮:进行k/2次,每次处理2n个数字;

第二轮:进行k/4次,每次处理4n个数字;

。。。。。。

第(2^logk/2)轮(最后一轮):进行k/(2^logk)次,每次处理2^logk*N个值。

推得时间复杂度:2N*k/2 + 4N*k/4 + 8N * k/8 + ... +2^logk * N * k/(2^logk) = Nk + Nk + ... + Nk = O(kNlogk)

 代码实现

#include <stdio.h>

struct ListNode {
 	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};

#include <vector>

class Solution {
public:
	ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    	ListNode temp_head(0);
    	ListNode *pre = &temp_head;
    	while (l1 && l2){
	    	if (l1->val < l2->val){
	    		pre->next = l1;
	    		l1 = l1->next;
	    	}
	    	else{
	    		pre->next = l2;
	    		l2 = l2->next;
	    	}
	    	pre = pre->next;
	    }
	    if (l1){
    		pre->next = l1;
    	}
    	if (l2){
	    	pre->next = l2;
	    }
        return temp_head.next;
    }	
    ListNode* mergeKLists(std::vector<ListNode*>& lists) {
    	if (lists.size() == 0){
        	return NULL;
        }
    	if (lists.size() == 1){
	    	return lists[0];
	    }
	    if (lists.size() == 2){
    		return mergeTwoLists(lists[0], lists[1]);
    	}
    	int mid = lists.size() / 2;
    	std::vector<ListNode*> sub1_lists;
    	std::vector<ListNode*> sub2_lists;
    	for (int i = 0; i < mid; i++){
	    	sub1_lists.push_back(lists[i]);
	    }
	    for (int i = mid; i < lists.size(); i++){
    		sub2_lists.push_back(lists[i]);
    	}
    	ListNode *l1 = mergeKLists(sub1_lists);
    	ListNode *l2 = mergeKLists(sub2_lists);
    	return mergeTwoLists(l1, l2);
    }
};

int main(){
	ListNode a(1);
	ListNode b(4);
	ListNode c(6);
	ListNode d(0);
	ListNode e(5);
	ListNode f(7);
	ListNode g(2);
	ListNode h(3);
	a.next = &b;
	b.next = &c;	
	d.next = &e;
	e.next = &f;	
	g.next = &h;
	Solution solve;	
	std::vector<ListNode *> lists;
	lists.push_back(&a);
	lists.push_back(&d);
	lists.push_back(&g);	
	ListNode *head = solve.mergeKLists(lists);
	while(head){
		printf("%d\n", head->val);
		head = head->next;
	}
	return 0;
}

经验总结

  • 遇到链表合并类的题目时候,有四种思路:1)暴力合并,按顺序一个一个的合并;2)借用STL算法,把每个节点重排;3)用分治法,进行两两归并。

这三种方法的时间复杂度依次递减,后者最优,要根据时间情况来选择使用。

发布了271 篇原创文章 · 获赞 8 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_17846375/article/details/104539904