本文是剑指offer链表专题
从尾到头打印链表
题目描述
输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
算法思想:
建立栈来实现,将链表中所有元素都进栈后,出栈的顺序就是链表从尾到头打印的顺序。
代码:
//链表结点定义
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) :
* val(x), next(NULL) {
* }
* };
*/
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
int i,j;
stack<ListNode*>Node;//建立栈
ListNode* pNode=head;
//链表全部进栈
while(pNode!=NULL)
{
Node.push(pNode);
pNode=pNode->next;//有一个问题:单循环链表的尾结点的指针域的值是什么?
}
vector<int>ArrayList;
//依次出栈
while(!Node.empty())
{
pNode=Node.top();
ArrayList.push_back(pNode->val);
Node.pop();
}
return ArrayList;
}
};
有一个问题:单循环链表的尾结点的指针域的值是什么?答:NULL
引申
栈本身也是一个递归结构,能用栈写就能用递归来做,能用递归写就能用循环写,但有个问题,当链表非常长的时候,就会导致函数条用的层级很深,从而有可能导致函数调用栈溢出,显然用循环实现的代码的鲁棒性要好一些。
浅谈下递归:
- 递归虽然有简洁的优点,但它同时也有显著的缺点。递归由于是函数条用自身,而函数条用是有时间和空间消耗的;每一次函数条用,都需要在内存栈中分配空间以保存参数、返回地址和临时变量,而且往栈里压入数据和弹出数据都需要时间。
- 递归很多计算是重复的,从而对性能带来很大影响。详情见下图,例如f(8)就计算了两次,f(7)计算了三次,f(6)计算了三次等等,按照递归求斐波那契数列的时间复杂度是O(2^n);
- 除了效率之外,递归可能引起更严重的问题:调用栈溢出,1中提到的每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多的时候,就会超出栈的容量,从而导致调用栈溢出。
链表中倒数第k个结点
题目描述
输入一个链表,输出该链表中倒数第k个结点
算法思想:
这道题必须要有一个原则,想要比较合适的时间复杂度,就必须之遍历一遍链表。
- 我们定义两个指针pfront,p2later,这里与剑指offer-和为S的连续正数序列、和为S的两个数字里面设置的指针不同,是实打实的指针,不是下标了,因为链表在内存地址中不是连续的,没法用连续的下标表示。
- plater指向头结点,pfront往前走k-1步
- 两个指针同时向前走,当pfornt走到尾结点时,plater指向的结点就是链表倒数第K个结点
代码:
//定义链表结点
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
int i,j,q=2,l=87;
ListNode *pfront=NULL;
ListNode *plater=NULL;
if((pListHead==NULL)||(k==0))//注意代码的鲁棒性
{
return NULL;
}
pfront=pListHead;
if(k==1)//注意代码的鲁棒性,当k就等于1的时候
{
while(pfront->next!=NULL)
{
pfront=pfront->next;
}
return pfront;
}
//pfornt往前走k-1个结点
for(i=0;i<(k-1);i++)
{
if(pfront->next!=NULL)
{
pfront=pfront->next;
}
else{
return NULL;
}
}
//plater指向头结点
plater=pListHead;
//两个结点同时走,当pfornt走到尾结点时,plater指向的结点就是链表倒数第K个结点
while(pfront->next!=NULL)
{
pfront=pfront->next;
plater=plater->next;
}
return plater;
}
};
需要注意的点:第一个for循环中,要考虑k的值大于整个链表长度的时候,这时候必须要加判断语句if(pfront->next!=NULL)才行
反转链表
题目描述:
输入一个链表,反转链表后,输出新链表的表头。
算法思想:
这里需要借助一张图来更为形象的表达:
不难注意到由于结点i的next指向了它的前一个结点,导致我们无法再链表中遍历到结点j。为了避免链表在结点i处断开,我们需要在调整结点i的next之前,把结点j保存下来
也就是说我们在调整结点i的next指针时,除了需要知道结点i本身之外,还需要i的前一个结点,因为我们需要把结点i的next指向h。同时,我们还要事先保存i的后一个结点j,以防止链表断开。
1.定义三个指针,分别指向当前遍历到的结点、它的前一个结点以及后一个结点,注意所有新建立的链表结点都需要初始化。
- 当前结点指针反转
- 当前结点反转好了,该下一个结点反转了
代码:
//链表结点定义、
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
int i,j;
ListNode *pCur;//当前结点
ListNode *pNext;//当前结点的下一个结点
ListNode *pNew=NULL;//用来复制当前结点
ListNode *pPre=NULL;//当前结点的前一个结点
if(pHead==NULL)//注意代码的鲁棒性
{
return NULL;
}
pCur=pHead;
while(pCur!=NULL)
{
pNext=pCur->next;
if(pNext==NULL)//注意代码的鲁棒性,链表中只有一个结点的情况
{
pNew=pCur;
}
pCur->next=pPre;//指针反转,这句没看懂
//当前结点反转好了,该下一个结点反转了
pPre=pCur;
pCur=pNext;
}
return pNew;//反转链表的头结点就是原始链表的尾结点
}
};
编程过程注意两点: 1.所有新建立的链表结点都需要初始化 2.注意代码鲁棒性,链表没有结点或者链表只有一个结点的情况
合并两个排序的链表
题目描述:
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
算法思想:
- 建立一个新的链表头结点
- 建立两个指针p1和p2,分别指向链表1和链表2的头结点
- 当链表1的头结点的值大于(小于)链表2的头结点的值时,链表2(链表1)的头结点是新链表的头结点,链表2的指针往后走一步
- 在剩余的结点中,链表1的结点小于(大于)链表2的结点,因此此时链表1的结点时新链表下一个结点,链表1的指针往后走一步
- 一直循环直到遍历完链表1和链表2
代码:
//链表结点定义
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
int i,j;
ListNode *pNodeHead=NULL;
ListNode *pNode=NULL;
if(pHead1==NULL)//注意代码鲁棒性
{
return pNode=pHead2;
}
if(pHead2==NULL)//注意代码鲁棒性
{
return pNode=pHead1;
}
if(pHead1->val>pHead2->val)
{
pNodeHead=pHead2;
pHead2=pHead2->next;
}
if(pHead1->val<=pHead2->val)
{
pNodeHead=pHead1;
pHead1=pHead1->next;
}
pNode=pNodeHead;
//pNode=pNode->next;
while((pHead1&&pHead2))
{
if(pHead1->val>pHead2->val)
{
pNode->next=pHead2;//这步做好后pNode的下一个结点确实是pHead,但pNode还指向前一个结点,从而需要pNode=pNode->next这就话
pHead2=pHead2->next;
pNode=pNode->next;
}
if(pHead1->val<pHead2->val)
{
pNode->next=pHead1;
pHead1=pHead1->next;
pNode=pNode->next;
}
else
{
pNode->next=pHead1;
pHead1=pHead1->next;
pNode=pNode->next;
}
}
//下面是链表1和链表2长度不一致的情况,假设我链表1先遍历好,
//那么新链表的接下来就是链表2的剩余了,不用比较,链表2也是排好序的
if(pHead1==NULL)
{
pNode->next=pHead2;
}
if(pHead2==NULL)
{
pNode->next=pHead1;
}
return pNodeHead;
}
};
注意一点:pNode->next=pHead2这就话之后pNode的下一个结点确实是pHead2,但pNode还指向前一个结点,就像从而需要pNode=pNode->next这就话,如下图所示:
复杂链表的复制
题目描述:
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
算法思想:
- 复制每一个结点,将复制好的结点插入对应结点的后面,例如复制A的结点为A1,那么就将A1插入到A后面
- 遍历链表:A1->random = A->random->next
- 将链表拆分成原链表和复制后的链表
代码:
/*
struct RandomListNode {
int label;
struct RandomListNode *next, *random;
RandomListNode(int x) :
label(x), next(NULL), random(NULL) {
}
};
*/
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead)
{
int i,j;
if(!pHead)//注意代码的鲁棒性
{
return NULL;
}
RandomListNode *Cur1,*Cur2;
Cur1=pHead;
//第一步:复制每一个结点,将复制好的结点插入对应结点的后面,例如复制A的结点为A1,那么就将A1插入到A后面
while(Cur1)
{
RandomListNode *temp1=new RandomListNode(Cur1->label);
temp1->next=Cur1->next;
Cur1->next=temp1;
Cur1=temp1->next;
}
Cur2=pHead;
//第二步:遍历链表:A1->random = A->random->next
while(Cur2)
{
RandomListNode *temp2=Cur2->next;
RandomListNode *tmp=Cur2;
if(Cur2->random)//要加这一句
{
temp2->random=tmp->random->next;
}
Cur2=Cur2->next->next;
}
//第三步:将链表拆分成原链表和复制后的链表
RandomListNode *pClone;
RandomListNode *temp3;
Cur1=pHead;
pClone=pHead->next;
while(Cur1->next)
{
temp3=Cur1->next;
Cur1->next=temp3->next;
Cur1=temp3;
}
return pClone;
}
};
两个链表的第一个公共结点
题目描述:
输入两个链表,找出它们的第一个公共结点。
算法思想:
两个链表如果存在公共结点,那么它们在这个结点之后的链表是一样的,可以先求出两个链表的长度。如果两个链表一样长,就一起从头开始走;如果两个链表不一样长,就让长的链表先走两个链表的长度差,再一起走。注意代码的鲁棒性
代码:
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
int len1,len2;
ListNode *p1;
ListNode *p2;
p1=pHead1;
p2=pHead2;
len1=fun_length(pHead1);
len2=fun_length(pHead2);
int temp;
if(len1==0||len2==0)//注意代码鲁棒性
{
return NULL;
}
if(len1==len2)
{
while(p1!=p2)
{
p1=p1->next;
p2=p2->next;
}
}
else if(len1>len2)
{
temp=len1-len2;
p1=fun_walk(p1,temp);
while(p1!=p2)
{
p1=p1->next;
p2=p2->next;
}
}
else
{
temp=len2-len1;
p2=fun_walk(p2,temp);
while(p1!=p2)
{
p1=p1->next;
p2=p2->next;
}
}
//求两个链表长度差值
return p1;
}
//求链表的长度
int fun_length(ListNode *p)
{
int result;
if(p==NULL)//注意代码鲁棒性
{
result=0;
}
else{
result=1;
while(p)
{
p=p->next;
result++;
}
}
return result;
}
//让长的链表先走长度差值步
ListNode* fun_walk(ListNode *p1,int temp)
{
while(temp!=0)
{
p1=p1->next;
temp--;
}
return p1;
}
};