剑指offer--链表
第1题:从尾到头打印链表
思路
递归
我们都知道链表无法逆序访问,那肯定无法直接遍历链表得到从尾到头的逆序结果。但是我们都知道递归是到达底层后才会往上回溯,因此我们可以考虑递归遍历链表,因此三段式如下:
终止条件: 递归进入链表尾,即节点为空节点时结束递归。
返回值: 每次返回子问题之后的全部输出。
本级任务: 每级子任务递归地进入下一级,等下一级的子问题输出数组返回时,将自己的节点值添加在数组末尾。
具体做法:
step 1:从表头开始往后递归进入每一个节点。
step 2:遇到尾节点后开始返回,每次返回依次添加一个值进入输出数组。
step 3:直到递归返回表头。
答案
class Solution {
public:
void recursion(ListNode* head, vector<int> &res){
if(head==nullptr) return;
else{
recursion(head->next,res);
res.push_back(head->val);
}
return ;
}
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> res;
recursion(head,res);
return res;
}
};
模拟栈
答案
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> res;
stack<int> st;
while(head!=nullptr){
st.push(head->val);
head = head->next;
}
while(!st.empty()){
int val = st.top();
st.pop();
res.emplace_back(val);
}
return res;
}
};
附加:翻转链表
思路
双指针
两个指针 pre cur
初始化:cur = head; pre = NULL
遍历终止条件:while(cur)
temp = cur->next
cur->next =pre
pre = cur
cur = temp
return pre
答案
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
};
递归
reverse(head, NULL)
递归终止 if(cur==NULL) return pre
递归逻辑 temp = cur->next
cur->next = pre
reverse(temp,cur)
答案
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}
};
第2题:合并两个排序的链表
思路
变量:准备四个指针
dummy, pre, l1, l2
逻辑:每一轮对l1和l2指向结点比较,让pre指向较小的那一个,并让较小的结点像后指
结束条件:l1或l2指向nullptr
答案
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead1 ListNode类
* @param pHead2 ListNode类
* @return ListNode类
*/
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
// write code here
// write code here
auto dummy = new ListNode(-1);
ListNode* pre = dummy;
while(pHead1!=nullptr&&pHead2!=nullptr){
if(pHead1->val <= pHead2->val){
pre->next = pHead1;
pHead1 = pHead1->next;
}else{
pre->next = pHead2;
pHead2 = pHead2->next;
}
pre = pre->next;
if(!dummy->next) dummy->next = pre;
}
//如果pHead1中还有结点
if(pHead1!=nullptr) pre->next = pHead1;
//如果pHead2中还有结点
if(pHead2!=nullptr) pre->next = pHead2;
return dummy->next;
}
};
第3题:两个链表的第一个公共结点
描述
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。
第4题:两个链表的第一个公共结点
描述
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
TC: O(n)
SC: O(1)
思路
双指针
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
ListNode *ta = pHead1, *tb = pHead2;
while (ta != tb) {
ta = ta ? ta->next : pHead2;
tb = tb ? tb->next : pHead1;
}
return ta;
}
};
第5题:链表中环的入口结点
- 题目描述:给定一条链表,若链表存在环,就请找到环的入口并返回入口的指针;若不存在环就返回null
- 思路:快慢指针
- 答案:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
ListNode *fast = pHead, *slow = pHead; // 快慢指针一开始都指向头
while(fast){
slow = slow->next; // 慢指针走一步
if(fast->next == nullptr) return nullptr; // 若快指针的下一步不能走,则说明两指针不会相遇
fast = fast->next->next; // 快指针向后走两步
if(fast == slow){ // 找到相交节点, 此时慢指针已经走了nb步
fast = pHead; // 快指针重新移动到头
while(fast != slow){ // 直到两指针相遇位置,每次向后走一步
fast = fast->next;
slow = slow->next;
}
return fast; // 找到入口节点,直接返回
}
}
return nullptr;
}
第6题:链表中倒数最后k个结点
-
题目描述
输入一个长度为n的链表,设链表中的元素的值为\(a_i\),返回该链表中的第k个结点。
如果该链表长度小于\(k\),请返回一个长度为0的链表 -
思路
双指针- step1: 准备一个快指针,从链表头开始,在链表上先走k步。
- step2: 准备慢指针指向原始链表头,代表当前元素,则慢指针与快指针之间的距离一致都是k。
- step3:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数k个元素的位置。
特点:双指针的初始位置、快慢。
-
答案
class Solution {
public:
ListNode* FindKthToTail(ListNode* pHead, int k) {
ListNode* fast = pHead;
ListNode* slow = pHead;
//快指针先行k步
for(int i = 0; i < k; i++){
if(fast != NULL)
fast = fast->next;
//达不到k步说明链表过短,没有倒数k
else
return slow = NULL;
}
//快慢指针同步,快指针先到底,慢指针指向倒数第k个
while(fast != NULL){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
第7题:复杂链表的复制
- 题目描述
输入一个复杂链表(每个结点有结点值,以及两个指针,一个指向下一个结点,另一个特殊指针random指向一个随机结点),请对此链表进行深拷贝。(注意:输出结果中请不要返回参数中的结点引用,否则判题程序会直接返回空)。下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。 - 思路
组合链表(双指针)
正常链表的复制,从头到尾遍历链表,对于每个结点创建新的结点,赋值,并将其连接好就可以了。这道题的不同之处在于我们还要将随机指针连接好,我们创建结点的时候,有可能这个结点创建了,但是它的随机指针指向的结点没有创建,因此创建的时候只能连接指向后面的指针,无法连接随机指针。
等链表连接好了,再连接随机指针的话,我们又难以找到这个指针指向的位置,因为链表不支持随机访问。但是吧,我们待拷贝的链表可以随机指针访问节点,那么我们不如将拷贝后的每个结点插入到原始链表相应结点之后,这样连接random指针的时候,原始链表random指针后一个元素就是原始链表要找的随机节点,而该节点后一个就是它拷贝出来的新节点,则就可以连上了。- step1:遍历链表,对每个结点新建一个拷贝结点,并插入到该节点之后。
- step2:使用双指针再次遍历链表,两个指针每次移动两步,一个指针遍历原始节点,一个指针遍历拷贝节点,拷贝节点的随机指针跟随原始节点,指向原始节点随机指针的下一位。
- step3:再次使用双指针遍历链表,每次越过一位后相连,即拆分成两个链表。
- 答案
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
//空节点直接返回
if(pHead == null)
return pHead;
//添加一个头部节点
RandomListNode cur = pHead;
//遍历原始链表,开始复制
while(cur != null){
//拷贝节点
RandomListNode clone = new RandomListNode(cur.label);
//将新节点插入到被拷贝的节点后
clone.next = cur.next;
cur.next = clone;
cur = clone.next;
}
cur = pHead;
RandomListNode clone = pHead.next;
RandomListNode res = pHead.next;
//连接新链表的random节点
while(cur != null){
//跟随前一个连接random
if(cur.random == null)
clone.random = null;
else
//后一个节点才是拷贝的
clone.random = cur.random.next;
//cur.next必定不为空
cur = cur.next.next;
//检查末尾节点
if(clone.next != null)
clone = clone.next.next;
}
cur = pHead;
clone = pHead.next;
//拆分两个链表
while(cur != null){
//cur.next必定不为空
cur.next = cur.next.next;
cur = cur.next;
//检查末尾节点
if(clone.next != null)
clone.next = clone.next.next;
clone = clone.next;
}
return res;
}
}
第8题:删除链表中重复的结点
描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5
TC: O(n)
SC: O(n)
思路
直接比较删除(推荐使用)
这是一个升序列表,重复的节点都连在一起,我们就可以很轻易地比较到重复的节点,然后将所有的连续相同的节点都跳过,连接不相同的第一个节点。
step1: 给链表前加上表头,方便可能的话删除第一个节点
step2: 遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去
step3: 在step2中这一连串相同的节点直接脸上后续第一个不相同值的节点。
step4:返回时去掉添加的表头
答案
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead) {
//空链表
if(pHead == NULL)
return NULL;
ListNode* res = new ListNode(0);
//在链表前加一个表头
res->next = pHead;
ListNode* cur = res;
while(cur->next != NULL && cur->next->next != NULL){
//遇到相邻两个节点值相同
if(cur->next->val == cur->next->next->val){
int temp = cur->next->val;
//将所有相同的都跳过
while (cur->next != NULL && cur->next->val == temp)
cur->next = cur->next->next;
}
else
cur = cur->next;
}
//返回时去掉表头
return res->next;
}
};
哈希表
这道题幸运的是链表有序,我们可以直接与旁边的元素比较,然后删除重复。那我们扩展一点,万一遇到的链表无序呢?我们这里给出一种通用的解法,有序无序都可以使用,即利用哈希表来统计是否重复。
step1: 遍历一次链表,用哈希表记录每个节点值出现的次数。
step2: 在链表前加一个节点值为0的表头,方便可能的话删除表头元素。
step3: 再次遍历该链表,对于每个节点值检查哈希表中的计数,只留下计数为1的,其他情况都删除
step4: 返回时去掉增加的表头。
答案
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead) {
//空链表
if(pHead == NULL)
return NULL;
unordered_map<int, int> mp;
ListNode* cur = pHead;
//遍历链表统计每个节点值出现的次数
while(cur != NULL){
mp[cur->val]++;
cur = cur->next;
}
ListNode* res = new ListNode(0);
//在链表前加一个表头
res->next = pHead;
cur = res;
//再次遍历链表
while(cur->next != NULL){
//如果节点值计数不为1
if(mp[cur->next->val] != 1)
//删去该节点
cur->next = cur->next->next;
else
cur = cur->next;
}
//去掉表头
return res->next;
}
};
第9题 删除链表的节点
描述
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
1.此题对比原题有改动
2.题目保证链表中节点的值互不相同
3.该题只会输出返回的链表和结果做对比,所以若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点
思路
迭代遍历(推荐使用)
既然是整个链表元素都不相同,我们要删除给定的一个元素,那我们首先肯定要找到这个元素,然后考虑删除它。
删除一个链表节点,肯定是断掉它的前一个节点指向它的指针,然后指向它的后一个节点,即越过了需要删除的这个节点。
step 1: 首先我们加入一个头部节点,方便于如果可能的话删除掉第一个元素
step 2: 准备两个指针遍历链表,一个指针指向当前要遍历的元素,另一个指针指向该元素的前序节点,便于获取它的指针。
step 3: 遍历链表。找到目标节点,则断开连接,指向后一个。
step 4: 返回时去掉我们加入的头节点。
答案
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
//加入一个头节点
ListNode* res = new ListNode(0);
res->next = head;
//前序节点
ListNode* pre = res;
//当前节点
ListNode* cur = head;
//遍历链表
while(cur != NULL){
//找到目标节点
if(cur->val == val){
//断开连接
pre->next = cur->next;
break;
}
pre = cur;
cur = cur->next;
}
//返回去掉头节点
return res->next;
}
};