数据结构之链表

来源:互联网 发布:网络直播股票 编辑:程序博客网 时间:2024/05/01 11:58

常用链表定义:

public class ListNode  {      int val;      ListNode next;      ListNode(int x)      {          val=x;      }  } 

单链表的创建与遍历

public class LinkList {    public Node head;    public Node current;    //方法:向链表中添加数据    public void add(int data) {        //判断链表为空的时候        if (head == null) {//如果头结点为空,说明这个链表还没有创建,那就把新的结点赋给头结点            head = new Node(data);            current = head;        } else {            //创建新的结点,放在当前节点的后面(把新的结点合链表进行关联)            current.next = new Node(data);            //把链表的当前索引向后移动一位            current = current.next;   //此步操作完成之后,current结点指向新添加的那个结点        }    }   public void print(Node node) {        if (node == null) {            return;        }        current = node;        while (current != null) {            System.out.println(current.data);            current = current.next;        }    }    class Node {        //注:此处的两个成员变量权限不能为private,因为private的权限是仅对本类访问。        int val; //数据域        Node next;//指针域        public Node(int val) {            this.val= val;        }    }}

复杂链表的复制

问题

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回这个链表的复制。

问题链接

https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&rp=2&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

参考地址

http://wiki.jikexueyuan.com/project/for-offer/question-twenty-six.html

代码实现

/*public class RandomListNode {    int label;    RandomListNode next = null;    RandomListNode random = null;    RandomListNode(int label) {        this.label = label;    }}*///参考链接:http://wiki.jikexueyuan.com/project/for-offer/question-two.htmlpublic class Solution {    public RandomListNode Clone(RandomListNode pHead)    {        if(pHead==null)            return pHead;        //为每一个结点创建一个副本结点        RandomListNode newHead=pHead;        while(newHead!=null){            RandomListNode node=new RandomListNode(newHead.label);            node.next=newHead.next;            newHead.next=node;            newHead=newHead.next.next;        }        //处理random结点        RandomListNode newHead2=pHead;        while(newHead2!=null){            //注意这里,空指针异常            RandomListNode next=newHead2.next;            RandomListNode random=newHead2.random;            if(random!=null)                next.random=random.next;            newHead2=next.next;        }        //拆分原链表和副本链表        RandomListNode cloneNode=pHead.next;//副本链表的头结点        RandomListNode newHead3=pHead;//原链表的遍历指针        while(newHead3!=null){            RandomListNode next=newHead3.next;//副本链表            newHead3.next=next.next;            newHead3=newHead3.next;            if(newHead3!=null)                next.next=newHead3.next;        }        return cloneNode;    }}

二叉搜索树转为有序的双向链表

问题

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&rp=2&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking

代码实现:

/**public class TreeNode {    int val = 0;    TreeNode left = null;    TreeNode right = null;    public TreeNode(int val) {        this.val = val;    }}*/public class Solution {    TreeNode phead;    TreeNode realhead;    public TreeNode Convert(TreeNode pRootOfTree) {        if(pRootOfTree==null)            return pRootOfTree;        ConvertSub(pRootOfTree);        return realhead;    }    //中序遍历    public void ConvertSub(TreeNode root){        if(root==null)            return ;        ConvertSub(root.left);        if(phead==null){            phead=root;            realhead=root;        }        else{            phead.right=root;            root.left=phead;            phead=root;        }        ConvertSub(root.right);    }}

以下内容为转载
原文:http://wuchong.me/blog/2014/03/25/interview-link-questions/

在O(1)时间删除链表节点

题目描述:给定链表的头指针和一个节点指针,在O(1)时间删除该节点。[Google面试题]

分析:要想在O(1)时间搞定,绝对不能用遍历。,本题与《编程之美》上的「从无头单链表中删除节点」类似。主要思想都是「狸猫换太子」,即用下一个节点数据覆盖要删除的节点,然后删除下一个节点。

特殊情况
1.如果要删除的节点没有下一个节点:
则要找到上一个节点,并且将next设置为空,这个时候只能用遍历

2.如果当前链表就一个节点
由于java中没有指向指针的指针,所以用返回null。

代码实现:

//O(1)时间删除链表节点,从无头单链表中删除节点

/**     * 在时间复杂度为O(1)的情况下删除链表的节点。<br/>     * 不过有个前提是要删除的这个节点必须在链表中包含,不然不会报错也不会改变原先的链表     * */    public  Node delete(Node head, Node node) {        // 检查空        if (node == null) {            throw new RuntimeException();        }        Node next = node.next;        if (next != null) {            // 有下一个节点的情况:复制覆盖            node.value = next.value;            node.next = next.next;            // 没有下一个节点的情况        } else {            Node previous = null;            Node current = head;            while (current != null) {                if (current == node) {                    // 前一个为空,要删除的节点即是第一个头结点                    if (previous == null) {                        head = null;                    } else {                        // 前一个不为空,则将next置为null即可                        previous.next = null;                    }                }                previous = current;                current = current.next;            }        }        return head;    }

平均时间复杂度[(n-1)*O(1)+O(n)]/n,仍然为O(1)

2. 单链表的转置

题目描述:

给一个单向链表,把它从头到尾反转过来。比如: a -> b -> c ->d 反过来就是 d -> c -> b -> a 。

分析:

链表的转置是一个很常见、很基础的数据结构题了,非递归的算法很简单,用三个临时指针 pre、head、next 在链表上循环一遍即可。递归算法也是比较简单的,但是如果思路不清晰估计一时半会儿也写不出来吧。

非递归

//单链表的转置,非递归public Node reverse(Node current) {      //初始化    Node previousNode = null;      Node nextNode = null;      while (current != null) {          //保留下一个结点        nextNode = current.next;          //将当前结点的下一个指针指向previous结点,即翻转        current.next = previousNode;          //移动指针        previousNode = current;          current = nextNode;               }      return previousNode;  } 

递归方法

public Node reverse(Node current)   {       if (current == null || current.next == null) return current;       Node nextNode = current.next;       Node reverseRest = reverse(nextNode);       nextNode.next = current;       current.next = null;  //防止循环链表     return reverseRest;   }  

递归的方法其实是非常巧的,它利用递归走到链表的末端,然后再更新每一个node的next 值 (代码倒数第二句)。 在上面的代码中, reverseRest 的值没有改变,为该链表的最后一个node,所以,反转后,我们可以得到新链表的head。

3. 求链表倒数第k个节点

题目描述:

输入一个单向链表,输出该链表中倒数第k个节点,链表的倒数第0个节点为链表的尾指针。

分析:

设置两个指针 p1、p2,首先 p1 和 p2 都指向 head,然后 p2 向前走 k 步,这样 p1 和 p2 之间就间隔 k 个节点,最后 p1 和 p2 同时向前移动,直至 p2 走到链表末尾。

代码如下:

 public ListNode FindKthToTail(ListNode head,int k) {            if (k <= 0 || head == null) {                return null;            }            int index = 0;            ListNode targetNode = head;            ListNode node = head;            int len= 0;//统计长度            while (node != null) {                if (index >= k) {                    targetNode = targetNode.next;                }                index++;                node = node.next;                len++;            }            // 是否k的值大于了链表的长度            if (len< k) {                return null;            }            return targetNode;        }

4. 求链表的中间节点

题目描述:

求链表的中间节点,如果链表的长度为偶数,返回中间两个节点的任意一个,若为奇数,则返回中间节点。

分析:

此题的解决思路和第3题「求链表的倒数第 k 个节点」很相似。可以先求链表的长度,然后计算出中间节点所在链表顺序的位置。但是如果要求只能扫描一遍链表,如何解决呢?最高效的解法和第3题一样,通过两个指针来完成。用两个指针从链表头节点开始,一个指针每次向后移动两步,一个每次移动一步,直到快指针移到到尾节点,那么慢指针即是所求。

代码如下:

   /**      * 此题可应用于上一题类似的思想。也是设置两个指针,只不过这里是,两个指针同时向前走,前面的指针每次走两步,后面的指针每次走一步     * 前面的指针走到最后一个结点时,后面的指针所指结点就是中间结点,即第(n/2+1)个结点。注意链表为空,链表结点个数为1和2的情况。时间复杂度O(n )     */      public Node getMiddleNode(Node head) {          if (head == null || head.next == null) {              return head;          }          Node q = head;      // p---q           Node p = head;          // 前面指针每次走两步,直到指向最后一个结点,后面指针每次走一步          while (q.next != null) {              q = q.next;              p = p.next;              if (q.next != null) {                  q = q.next;              }          }          return p;      } 

5. 判断单链表是否存在环

题目描述:

输入一个单向链表,判断链表是否有环?

分析:

通过两个指针,分别从链表的头节点出发,一个每次向后移动一步,另一个移动两步,两个指针移动速度不一样,如果存在环,那么两个指针一定会在环里相遇,时间复杂度为O (n)。

代码实现:

  public  boolean hasCycle(Node head) {          Node fast = head; // 快指针每次前进两步          Node slow = head; // 慢指针每次前进一步          while (fast != null && fast.next != null) {              fast = fast.next.next;              slow = slow.next;              if (fast == slow) { // 相遇,存在环                  return true;              }          }          return false;      }

6. 找到环的入口点

题目描述:

输入一个单向链表,判断链表是否有环。如果链表存在环,如何找到环的入口点?

方案1

解题思路:

由上题可知,按照 p2 每次两步,p1 每次一步的方式走,发现 p2 和 p1 重合,确定了单向链表有环路了。接下来,让p2回到链表的头部,重新走,每次步长不是走2了,而是走1,那么当 p1 和 p2 再次相遇的时候,就是环路的入口了。

为什么?:假定起点到环入口点的距离为 a,p1 和 p2 的相交点M与环入口点的距离为b,环路的周长为L,当 p1 和 p2 第一次相遇的时候,假定 p1 走了 n 步。那么有:

p1走的路径: a+b = n;
p2走的路径: a+b+k*L = 2*n; p2 比 p1 多走了k圈环路,总路程是p1的2倍

根据上述公式可以得到 k*L=a+b=n显然,如果从相遇点M开始,p1 再走 n 步的话,还可以再回到相遇点,同时p2从头开始走的话,经过n步,也会达到相遇点M。

显然在这个步骤当中 p1 和 p2 只有前 a 步走的路径不同,所以当 p1 和 p2 再次重合的时候,必然是在链表的环路入口点上。

代码如下:

//找到环的入口点Node* findLoopPort(Node *head){    //如果head为空,或者为单结点,则不存在环    if(head == NULL || head->next == NULL) return NULL;    Node *slow,*fast;    slow = fast = head;    //先判断是否存在环    while(fast != NULL && fast->next != NULL)    {        fast = fast->next->next;        slow = slow->next;        if(fast == slow)            break;    }    if(fast != slow) return NULL;    //不存在环    fast = head;                //快指针从头开始走,步长变为1    while(fast != slow)            //两者相遇即为入口点    {        fast = fast->next;        slow = slow->next;    }    return fast;}

方案二,先求环的长度

解题思路:

这里我们需要先获取环的长度length。拿到环的长度length之后,需要用到两个指针变量first和second,先让second指针走length步;然后让first指针和second指针同时各走一步,当两个指针相遇时,相遇时的结点就是环的起始点。

注:为了找到环的起始点,我们需要先获取环的长度,而为了获取环的长度,我们需要先判断是否有环。所以这里面其实是用到了三个方法。

代码实现

 //方法:判断单链表是否有环。返回的结点是相遇的那个结点    public Node hasCycle(Node head) {        if (head == null) {            return null;        }        Node first = head;        Node second = head;        while (second != null) {            first = first.next;            second = second.next.next;            if (first == second) {  //一旦两个指针相遇,说明链表是有环的                return first;  //将相遇的那个结点进行返回            }        }        return null;    }    //方法:有环链表中,获取环的长度。参数node代表的是相遇的那个结点    public int getCycleLength(Node node) {        if (head == null) {            return 0;        }        Node current = node;        int length = 0;        while (current != null) {            current = current.next;            length++;            if (current == node) {  //当current结点走到原点的时候                return length;            }        }        return length;    }    //方法:获取环的起始点。参数length表示环的长度    public Node getCycleStart(Node head, int cycleLength) {        if (head == null) {            return null;        }        Node first = head;        Node second = head;        //先让second指针走length步        for (int i = 0; i < cycleLength; i++) {            second = second.next;        }        //然后让first指针和second指针同时各走一步        while (first != null && second != null) {            first = first.next;            second = second.next;            if (first == second) { //如果两个指针相遇了,说明这个结点就是环的起始点                return first;            }        }        return null;    }

7. 编程判断两个链表是否相交

题目描述:

给出两个单向链表的头指针(如下图所示),

这里写图片描述

比如h1、h2,判断这两个链表是否相交。这里为了简化问题,我们假设两个链表均不带环。

代码实现:

// 判断两个单链表是否相交      /**      * 如果两个链表相交于某一节点,那么在这个相交节点之后的所有节点都是两个链表所共有的。 也就是说,如果两个链表相交,那么最后一个节点肯定是共有的。      * 先遍历第一个链表,记住最后一个节点,然后遍历第二个链表, 到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交,      * 否则不相交。时间复杂度为O(len1+len2),因为只需要一个额外指针保存最后一个节点地址, 空间复杂度为O(1)      */      public static boolean isIntersect(Node head1, Node head2) {          if (head1 == null || head2 == null) {              return false;          }          Node h1= head1;          // 找到链表1的最后一个节点          while (h1.next != null) {              h1= h1.next;          }          Node h2 = head2;          // 找到链表2的最后一个节点          while (h2 .next != null) {              h2 = h2 .next;          }          return h1== h2 ;      }  

8. 扩展:链表有环,如何判断相交

题目描述:

上面的问题都是针对链表无环的,那么如果现在,链表是有环的呢?上面的方法还同样有效么?

分析:

如果有环且两个链表相交,则两个链表都有共同一个环,即环上的任意一个节点都存在于两个链表上

判断一个链表环上的一点,是否在另一个链表上

因此,就可以判断一链表上俩指针相遇的那个节点,在不在另一条链表上。

代码实现:

//判断两个带环链表是否相交
bool isIntersectWithLoop(Node *h1,Node *h2)
{
Node *circleNode1,*circleNode2;
if(!hasCircle(h1,circleNode1)) //判断链表带不带环,并保存环内节点
return false; //不带环,异常退出
if(!hasCircle(h2,circleNode2))
return false;

Node *temp = circleNode2->next;while(temp != circleNode2){    if(temp == circleNode1)        return true;    temp = temp->next;}return false;

}

9. 两链表相交的第一个公共节点

题目描述:

如果两个无环单链表相交,怎么求出他们相交的第一个节点呢?

分析:

采用对齐的思想。计算两个链表的长度 L1 , L2,分别用两个指针 p1 , p2 指向两个链表的头,然后将较长链表的 p1(假设为 p1)向后移动L2 - L1个节点,然后再同时向后移动p1 , p2,直到 p1 = p2。相遇的点就是相交的第一个节点。

代码实现:

//求两链表相交的第一个公共节点Node* findIntersectNode(Node *h1,Node *h2){    int len1 = listLength(h1);          //求链表长度    int len2 = listLength(h2);    //对齐两个链表    if(len1 > len2)    {        for(int i=0;i<len1-len2;i++)            h1=h1->next;    }    else     {        for(int i=0;i<len2-len1;i++)            h2=h2->next;    }    while(h1 != NULL)    {        if(h1 == h2)            return h1;        h1 = h1->next;        h2 = h2->next;        }    return NULL;}

合并两个有序的单链表

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

/*public class ListNode {    int val;    ListNode next = null;    ListNode(int val) {        this.val = val;    }}*/public class Solution {    public ListNode Merge(ListNode list1,ListNode list2) {        ListNode newlist=new ListNode(0);        ListNode listhead1=list1;        ListNode listhead2=list2;        if(list1==null)            return list2;        if(list2==null)            return list1;        ListNode result=newlist;        while(listhead1!=null&&listhead2!=null){            if(listhead1.val<=listhead2.val){                newlist.next=new ListNode(listhead1.val);                newlist=newlist.next;                listhead1=listhead1.next;            }            else{                newlist.next=new ListNode(listhead2.val);                newlist=newlist.next;                listhead2=listhead2.next;            }        }        while(listhead1!=null){            newlist.next=new ListNode(listhead1.val);            listhead1=listhead1.next;        }        while(listhead2!=null){            newlist.next= new ListNode(listhead2.val);            listhead2=listhead2.next;        }        return result.next;    }}

链表的展平

问题描述:

从一个标准的双向链表开始。设想每个元素除了next和previous指针之外,还有一个child指针,这个指针可能会指向一个单独的双向链表,也可能为空。这样就产生了一个多层的数据结构。

将这个链表展平,以使得所有节点都出现在同一层的双向链表上。给定第一层链表的head和tail。每个节点定义如下:

public class Node{    Node next;    Node prev;    Node child;    int value;}

问题分析:

从第一层开始遍历,每次遇到子节点的结点时,将这个子节点附加到第一层的结尾,并更新尾指针。

Start at the beginning of the first levelWhile you are not at the end of the first level    If the current node has a child        Append the child to the end of the first level        Update the tail pointer    Advance to next node

代码实现:

public Node tail;//尾指针已给出public void flattenList(Node head){    Node curNode=head;    while(curNode!=null){        if(curNode.child!=null){            append(curNode.child,tail);        }        curNode = curNode.next;    }}public append(Node child){    Node curNode;    tail.next=child;    child.prev=tail;    curNode=child;    //移动尾指针    while(curNode.next!=null){        curNode=curNode.next;    }    tail=curNode;//更新尾指针。}

取消链表展平

代码实现:

public void unflattenList(Node start,Node tail){    Node curNode;    exploreAndSeparate(start);    curNode=start;    while(curNode.next!=null){        curNode=curNode.next;    }    tail=curNode;}public void exploreAndSeparate(Node childListStart){    Node curNode;    while(curNode){        if(curNode.child!=null){            curNode.child.prev.next=null;            curNode.child.prev=null;        }        exploreAndSeparate(curNode.child);    }    curNode=curNode.next;}

删除链表中重复的结点

题目描述

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
链接
https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

代码实现

/* public class ListNode {    int val;    ListNode next = null;    ListNode(int val) {        this.val = val;    }}*/public class Solution {    public ListNode deleteDuplication(ListNode pHead)    {        if(pHead==null||pHead.next==null)            return pHead;       pHead = deleteSub(pHead);//处理以重复元素开头的元素       if(pHead==null||pHead.next==null)            return pHead;        ListNode pre=pHead;        ListNode cur=pre.next;        if(cur==null)            return pHead;        ListNode next=cur.next;        while(next!=null){            ListNode temp=null;            if(cur.val==next.val){                       temp=deleteSub(cur);                if(temp==null){//若处理重复元素后,返回为空                    pre.next=null;//更新指针,使其下一个结点为空                    return pHead;                }                 pre.next=temp;                 pre=pre.next;                 cur=pre.next;                if(cur==null)                    return pHead;                next=cur.next;            }            else{                pre=pre.next;                cur=pre.next;                if(cur==null)                     return pHead;                next=cur.next;            }        }        return pHead;    }    public ListNode deleteSub(ListNode pHead){//用于删除连续的重复元素,并返回下一个不重复的元素        if(pHead==null||pHead.next==null)            return pHead;        while(pHead.next.val==pHead.val){//处理111223344的情况            while(pHead.next.val==pHead.val){                pHead=pHead.next;                if(pHead.next==null)                    return null;            }            pHead=pHead.next;            if(pHead.next==null)                return pHead;        }        return pHead;    }}

二叉树的下一个结点

题目描述:

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

链接
https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&rp=3&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking

分析:

三种情况:

  1. 树为空,则返回null
  2. 当前结点有右子树,则返回右子树的最左结点
  3. 当前结点没有右子树,则遍历其父节点,当当前结点的父节点的左子树就是当前结点时,返回其父节点,就是最开始所求结点按中序遍历的下一个结点

针对第三种情况的分析

这里写图片描述

public class Solution {    public TreeLinkNode GetNext(TreeLinkNode pNode)    {        if(pNode==null)            return null;        if(pNode.right!=null){//右子树不为空            pNode=pNode.right;            while(pNode.left!=null)                pNode=pNode.left;            return pNode;        }        //pNode.next表示的是PNode的父节点。        while(pNode.next!=null){//没右子树,则找第一个当前节点是父节点左孩子的节点            if(pNode.next.left==pNode)                return pNode.next;            pNode=pNode.next;        }        return null;    }}

10. 总结

可以发现,在链表的问题中,通过两个的指针来提高效率是很值得考虑的一个解决方案,所以一定要记住这种解题思路。记住几种典型的链表问题解决方案,很多类似的题目都可以转换到熟悉的问题再解决。

0 0
原创粉丝点击