剑指offer-链表专栏

本文是剑指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

引申

栈本身也是一个递归结构,能用栈写就能用递归来做,能用递归写就能用循环写,但有个问题,当链表非常长的时候,就会导致函数条用的层级很深,从而有可能导致函数调用栈溢出,显然用循环实现的代码的鲁棒性要好一些。

浅谈下递归:
  1. 递归虽然有简洁的优点,但它同时也有显著的缺点。递归由于是函数条用自身,而函数条用是有时间和空间消耗的;每一次函数条用,都需要在内存栈中分配空间以保存参数、返回地址和临时变量,而且往栈里压入数据和弹出数据都需要时间。
  2. 递归很多计算是重复的,从而对性能带来很大影响。详情见下图,例如f(8)就计算了两次,f(7)计算了三次,f(6)计算了三次等等,按照递归求斐波那契数列的时间复杂度是O(2^n);
    斐波那契数列用递归来做,重复计算很多
  3. 除了效率之外,递归可能引起更严重的问题:调用栈溢出,1中提到的每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多的时候,就会超出栈的容量,从而导致调用栈溢出。

链表中倒数第k个结点

题目描述

输入一个链表,输出该链表中倒数第k个结点

算法思想:

这道题必须要有一个原则,想要比较合适的时间复杂度,就必须之遍历一遍链表。

  1. 我们定义两个指针pfront,p2later,这里与剑指offer-和为S的连续正数序列、和为S的两个数字里面设置的指针不同,是实打实的指针,不是下标了,因为链表在内存地址中不是连续的,没法用连续的下标表示。
  2. plater指向头结点,pfront往前走k-1步
  3. 两个指针同时向前走,当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.定义三个指针,分别指向当前遍历到的结点、它的前一个结点以及后一个结点,注意所有新建立的链表结点都需要初始化
2. 当前结点指针反转
3. 当前结点反转好了,该下一个结点反转了

代码:

//链表结点定义、
/*
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.注意代码鲁棒性,链表没有结点或者链表只有一个结点的情况

合并两个排序的链表

题目描述:

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

算法思想:

  1. 建立一个新的链表头结点
  2. 建立两个指针p1和p2,分别指向链表1和链表2的头结点
  3. 当链表1的头结点的值大于(小于)链表2的头结点的值时,链表2(链表1)的头结点是新链表的头结点,链表2的指针往后走一步
  4. 在剩余的结点中,链表1的结点小于(大于)链表2的结点,因此此时链表1的结点时新链表下一个结点,链表1的指针往后走一步
  5. 一直循环直到遍历完链表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。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

算法思想:

  1. 复制每一个结点,将复制好的结点插入对应结点的后面,例如复制A的结点为A1,那么就将A1插入到A后面
  2. 遍历链表:A1->random = A->random->next
  3. 将链表拆分成原链表和复制后的链表

代码:

/*
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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值