数据结构——链表面试题

目录

 

1、反转链表

    1)不断从原来链表中取出结点,头插到一个新链表上。

 2)定义三个结点进行反转。

2、删除链表中等于给定值 val 的所有节点。

    1)创建一个新链表。遍历原来的链表,如果不是val就插入新链表中。

    2)遍历链表,如果是val就直接删除。

3、将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(三种方法)

    1)创建新链表,进行尾插。

    2)与方法1)类似,只是修改了while循环的条件。

    3)递归方法。

4、编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。注意:分割后保证原来的数据顺序不变。

5、给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

    1)先求出链表的长度len,然后向后移动(len / 2)次即可。

    2)定义两个结点:一个结点为fast,每次循环移动两次;另一个为slow结点,每次循环移动一次。

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

    1)先求出链表的长度len,然后重新遍历向后移动(len - k)次即可。

    2)前后引用遍历。

7、链表的回文结构。

8、在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。

9、编写一个程序,找到两个单链表相交的起始节点。

10、给定一个链表,判断链表中是否有环。    

11、 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL。

12、给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝(深拷贝)。


1、反转链表

    1)不断从原来链表中取出结点,头插到一个新链表上。

        (1)需要一个新链表,并且是一个空链表。

                Node result = null;    // result:是新链表的第一个结点的引用

        (2)遍历原来的链表。

                Node cur = head;

                while(cur != null) {

                    Node next = cur.next;

                }

        (3)把每一个遍历到的结点cur,头插到新链表result上。

                ① 已经有结点

                ② cur.next = result;

                ③ 更新最新的第一个结点:    result = cur;

public ListNode reverseList(ListNode head) {

    ListNode result = null;

    ListNode cur = head;



    while(cur != null) {

        ListNode next = cur.next;

        cur.next = result;

        result = cur;

        cur = next;

    }

    return result;

}

 2)定义三个结点进行反转。

 

public ListNode reverseList(ListNode head) {
    if (head == null) {
        return null;
    }
    ListNode p1 = null;
    ListNode p2 = head;
    ListNode p3 = head.next;

    while (p2 != null) {
        p2.next = p1;
        p1 = p2;
        p2 = p3;
        if (p3 != null) {
            p3 = p3.next;
        }
    }
    return p1;
}

2、删除链表中等于给定值 val 的所有节点。

    1)创建一个新链表。遍历原来的链表,如果不是val就插入新链表中。

    public ListNode removeElements(ListNode head, int val) {
        ListNode tmpHead = new ListNode(-1);
        tmpHead.next = head;
        ListNode prev = tmpHead;
        ListNode cur = head;
        
        while (cur != null) {
            if (cur.val == val) {
                prev.next = cur.next;
            } else {
                prev = cur;
            }
            
            cur = cur.next;
        }
        
        return tmpHead.next;
    }

    2)遍历链表,如果是val就直接删除。

    public ListNode removeElements(ListNode head, int val) {
        ListNode prev = null;
        ListNode cur = head;
        
        while (cur != null) {
            if (cur.val == val) {
                if (cur == head) {
                    head = cur.next;
                } else {
                    prev.next = cur.next;
                }
            } else {
                prev = cur;
            }
            cur = cur.next;
        }
        return head;
    }

         优化:

    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return null;
        }

        ListNode prev = head;
        ListNode cur = head.next;
        
        while (cur != null) {
            if (cur.val == val) {
                prev.next = cur.next;
            } else {
                prev = cur;
            }
            cur = cur.next;
        }
        if (head.val == val) {
            head = head.next;
        }
        return head;
    }

3、将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(三种方法)

            

    1)创建新链表,进行尾插。

          ListNode first = l1;            ListNode second = l2;

          ListNode result = null;

          while(first != null && second != null) {

                if(first.val <=second.val) {

                        // 把first尾插到result中

                        // 分情况讨论

                }

                if(first == null) {

                        // 把second尾插到result中

                } else {

                        // 把first尾插到result中

                }

          }

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1 == null) {
            return l2;
        }
        if(l2 == null) {
            return l1;
        }
        ListNode first = l1;
        ListNode second = l2;
        ListNode result = null;
        ListNode last = null;
        
        while(first != null && second != null) {
            if(first.val <= second.val) {
                ListNode next = first.next;
                if(result == null) {
                    result = first;
                } else {
                    last.next = first;
                }
                last = first;
                first = next;
            } else {
                ListNode next = second.next;
                if(result == null) {
                    result = second;
                } else {
                    last.next = second;
                }
                last = second;
                second = next;
            }
        }
        if(first != null) {
            last.next = first;
        } else {
            last.next = second;
        }
        return result;
    }

    2)与方法1)类似,只是修改了while循环的条件。

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode cur1 = l1;
        ListNode cur2 = l2;
        
        while (cur1 != null || cur2 != null) {
            if (cur1 == null) {
                // 尾插 cur2
            } else if (cur2 == null) {
                // 尾插 cur1
            } else {
                if (cur1.val <= cur2.val) {
                }
            }
        }
    }

    3)递归方法。

        可以如下递归地定义在两个链表里的 merge 操作(忽略边界情况,比如空链表等):

            {    list1[0]+merge(list1[1:],list2)                list1[0]<list2[0]

            |    list2[0]+merge(list1,list2[1:])                otherwise

        也就是说,两个链表头部较小的一个与剩下元素的 merge 操作结果合并。

        直接将以上递归过程建模,首先考虑边界情况。 特殊的,如果 l1 或者 l2 一开始就是 null ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个的头元素更小,然后递归地决定下一个添加到结果里的值。如果两个链表都是空的,那么过程终止,所以递归过程最终一定会终止。

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        
        if (l1.val <= l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }

4、编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。注意:分割后保证原来的数据顺序不变。

        (1)有两个新链表(都需要尾插);

        (2)可能有一个链表不存在;

        (3)保证最终链表的最后一个结点的next是null。

    1)理想情况下,把大链表接到小链表后边。

    2)如果没有小链表,直接返回大链表(大链表可能为空)。保证,返回链表的最后一个结点.next == null。

    尾插       分情况讨论:

            (1)如果当前链表为空,要插入的结点就是链表的第一个结点

            (2)如果链表不为空,

                1. 先找到当前的最后一个结点

                2. 让当前的最后一个结点的 next = 要插入的结点

                3. 如果每次的最后一个结点都是我们插入的,可以记录上次插入的最后一个结点。

                4. 不要忘记更新最后一个结点

x = 5

    public ListNode partition(ListNode pHead, int x) {
        ListNode less = null;
        ListNode lessLast = null;

        ListNode great = null;
        ListNode greatLast = null;
        
        ListNode cur = pHead;

        while (cur != null) {
            if (cur.val < x) {
                if (less == null) {
                    less = cur;
                } else {
                    lessLast.next = cur;
                }
                lessLast = cur;
            } else {
                if (great == null) {
                    great = cur;
                } else {
                    greatLast.next = cur;
                }
                greatLast = cur;
            }
            cur = cur.next;
        }
        if (less == null) {
            return great;
        } else {
            lessLast.next = great;
            if (greatLast != null) {
                greatLast.next = null;
            }
            return less;
        }
    }

 

5、给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

    1)先求出链表的长度len,然后向后移动(len / 2)次即可。

    private int getLength(ListNode head) {
        int len = 0;
        for (ListNode cur = head; cur != null; cur = cur.next) {
            len++;
        }
        return len;
    }
    public ListNode middleNode(ListNode head) {
        int len = getLength(head);
        int midLen = len / 2;
        ListNode node = head;
        for (int i = 0; i < midLen; i++) {
            node = node.next;
        }
        
        return node;
    }

    2)定义两个结点:一个结点为fast,每次循环移动两次;另一个为slow结点,每次循环移动一次。

        规则:如果链表不为空,则先移动一次fast结点,如果此时fast结点不为空,则再分别移动一次slow结点和fast结点,如果此时fast结点仍不为空,继续循环。只要循环过程中fast结点为空,则跳出循环,返回slow结点即为所求中间结点。

    public ListNode middleNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        
        while (fast != null) {
            fast = fast.next;
            if (fast == null) {
                break;
            }
            slow = slow.next;
            fast = fast.next;
        }
        
        return slow;
    }

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

    1)先求出链表的长度len,然后重新遍历向后移动(len - k)次即可。

    public ListNode FindKthToTail(ListNode head,int k) {
        int len = 0;
        for (ListNode c = head; c != null; c = c.next) {
            len++;
        }
        if (len < k) {
            return null;
        }
        int steps = len - k;
        ListNode r = head;
        for (int i = 0; i < steps; i++) {
            r = r.next;
        }
        return r;
    }

    2)前后引用遍历。

        定义两个结点:一个结点为front结点,先移动k次停止;另一个为back结点,在front结点结束k次移动后开始和front结点同时移动,直到front结点为空结束循环,返回back结点即为所求结点。

    public ListNode FindKthToTail(ListNode head,int k) {
        ListNode front = head;
        ListNode back = head;
        
        for (int i = 0; i < k; i++) {
            if (front == null) {
                return null;
            }
            front = front.next;
        }
        
        while (front != null) {
            back = back.next;
            front = front.next;
        }
        
        return back;
    }

7、链表的回文结构。

    先找到链表的中间结点,将后半部分的结点逆置,再与前半部分的结点进行比较。如果有结点不相等则跳出循环,返回false;如果前半部分或者后半部分任一为空时仍没有结点不相等,则链表为回文结构,返回true。

    public ListNode getMid(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        
        while (fast != null) {
            fast = fast.next;
            if (fast == null) {
                break;
            }
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
    
    public ListNode reverse(ListNode head) {
        ListNode result = null;
        ListNode cur = head;

        while (cur != null) {
            ListNode next = cur.next;
            cur.next = result;
            result = cur;
            cur = next;
        }
        return result;
    }

    public boolean chkPalindrome(ListNode A) {
        ListNode mid = getMid(A);
        ListNode h2 = reverse(mid);
        ListNode n1 = A;
        ListNode n2 = h2;

        while (n1 != null && n2 != null) {
            if (n1.val != n2.val) {
                return false;
            }
            n1 = n1.next;
            n2 = n2.next;
        }
        return true;
    }

8、在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。

    定义三个结点:一个结点prev为空;一个结点p1 = head;另一个结点p2 = head.next。

        如果p1 != p2,则三个结点同时向后移动。

        否则,p1不动,p2走到第一个不相等的结点处,prev.next = p2,p1 = p2,p2 = p2.next。

    直到p2为kong。

    public ListNode deleteDuplication(ListNode pHead)
    {
        if (pHead == null) {
            return null;
        }
        
        ListNode prev = null;
        ListNode p1 = pHead;
        ListNode p2 = pHead.next;
        
        while (p2 != null) {
            if (p1.val != p2.val) {
                prev = p1;
                p1 = p2;
                p2 = p2.next;
            } else {
                while (p2 != null && p2.val == p1.val) {
                    p2 = p2.next;
                }
                
                if (prev == null) {
                    pHead = p2;
                } else {
                    prev.next = p2;
                }
                p1 = p2;
                if (p2 != null) {
                    p2 = p2.next;
                }
            }
        }
            
        return pHead;
    }

9、编写一个程序,找到两个单链表相交的起始节点。

    如下面的两个链表,在结点c1开始相交。

    首先排除两个单链表相交不可能出现的情况——形如“×”的交叉。因为单链表的next结点只能指向一个结点,两个链表一旦相交之后,一定重合(如上图所示)。

    1)首先计算两个链表的长度,并求得长度差diff。

    2)先让较长的链表向后走diff步;

        

    3)然后两个链表同时向后走,如果有两个结点相等的情况,则第一个相遇即为两链表相交。

        

    private int getLength(ListNode head) {
        int len = 0;
        for(ListNode c = head; c != null; c = c.next) {
            len++;
        }
        return len;
    }
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = getLength(headA);
        int lenB = getLength(headB);
        int diff = lenA - lenB;
        
        ListNode longer = headA;
        ListNode shorter = headB;
        if(lenA < lenB) {
            longer = headB;
            shorter = headA;
            diff = lenB - lenA;
        }
        
        for(int i = 0; i < diff; i++) {
            longer = longer.next;
        }
        
        while(longer != shorter) {
            longer = longer.next;
            shorter = shorter.next;
        }
        return longer;
    }

10、给定一个链表,判断链表中是否有环。    

    快慢指针:快的一次2步,慢的一次1步。如果有环,两者一定相遇;如果没有环,则不会相遇。(快的不能一次3步或者n(n>=3)步,有可能会一直错过)

    如果相遇,则链表带环;如果快的遇到null,则不带环,返回null。

        

        

        

        

        

        

        

    public boolean hasCycle(ListNode head) {
        //求相遇
        // 如果快的遇到null,表示没有环,直接返回null
        ListNode fast = head;
        ListNode slow = head;
        
        do {
            if(fast == null) {
                return false;
            }
            fast = fast.next;
            if(fast == null) {
                return false;
            }
            fast = fast.next;
            slow = slow.next;
        }while(fast != slow);
        return true;
    }

11、 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL。

    方法1:把带环问题转为相交问题。将相遇点的next设为null,将带环链表变成两个不带环的链表,求两个链表的相交点。

    方法2:一个引用从起点出发,一个引用从相遇点出发,都直走一步,一定会在环的入口点相遇。

         证明:

                

                1.慢引用走的距离        L + C    (L表示从起点到入环的第一个入口点的距离,C表示慢引用在环内走过的距离)

                2.快引用走的距离        L + C + (n + 1) * R    (n >= 0,表示快引用在环内走的圈数;R表示快引用在环内走过的距离)

                                               或    2 * (L + C) = 2 * L + 2 * C

                2 * L + 2 * C = L + C + n * R + R - C    ==>    L = n * R + (R - C)

    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        // fast遇到null,表示不带环,返回null
        // fast = slow,表示遇到相遇点了
        do {
            if(fast == null) {
                return null;
            }
            fast = fast.next;
            if(fast == null) {
                return null;
            }
            fast = fast.next;
            slow = slow.next;
        }while(fast != slow);
        
        // 求相遇
        // 如果快的遇到null,表示没有环,直接返回null
        // 相遇点出发 + 起点出发,最终相遇
        ListNode p = head;
        ListNode q = slow;
        while(p != q) {
            p = p.next;
            q = q.next;
        }
        return p;
    }

扩展:

    “相交 + 带环问题”:求两个链表是否带环或相交。

        此问题可以分为六种情况:1)两者均不带环也不相交,2)两者不带环但相交,3)有一条链表带环但不相交,4)两者均带环但不相交,5)两者相交与环外,6)两者相交与环内。

        思路分析:

            1. 如果两个链表都没有带环,区分是否相交的问题<情况1)和2)>。

            2. 一个带环,一个不带环<情况3)>。

            3. 两个都带环:求环的入口点

                    如果是一个交点<情况5)>。

                    否则,如果两个环的长度不相等<情况4)>。

                                相等:一个从环入口点A出发,走环的长度,如果能遇到入环点B则为<情况6)>,否则为<情况4)>。

 

12、给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝(深拷贝)。

        

    首先分析:复制普通链表(浅拷贝)。

public Node copy(Node head) {
    Node result = null;
    Node resultLast = null;
    for(Node cur = head; cur != null; cur = cur.next) {
        Node node = new Node();
        node.val = cur. val;
        if(result == null) {
            result = cur;
        } else {
            resultLast.next = cur;
        }
        resultLast = cur;
    }
    return result;
}

    本题的结点Node类的定义如下:

class Node {
    public int val;
    public Node next;
    public Node random;    // 没有作用
    public Node() {}
    public Node(int _val,Node _next,Node _random) {
        val = _val;
        next = _next;
        random = _random;
    }
}

   给定如下图所示的链表:

        

        分三步走:

        (1)把原链表变成当前形式,即变为“老 — 新 — 老 — 新 — …”的形式。用p1遍历原链表的每个结点,创建新结点p2,把p2插入到p1的后边。

        

        (2)处理random指向。使p1 = head,p2 = p1.next。

        

            只要p1.random不为空,让p2.random = p1.random.next。

        

            直至p1为空结束循环。

        

        (3)完成复制。创建一个新链表将,新结点一一完成拷贝。

        

 

class Solution {
    public Node copyRandomList(Node head) {
        if(head == null) {
            return null;
        }
        // 1)
        Node p1 = head;
        while(p1 != null) {
            Node p2 = new Node();
            p2.val = p1.val;
            p2.random = null;
            p2.next = p1.next;
            p1.next = p2;
            p1 = p2.next;
        }
        
        // 2)
        p1 = head;
        while(p1 != null) {
            Node p2 = p1.next;
            if(p1.random != null) {
                p2.random = p1.random.next;
            }
            p1 = p2.next;
        }
        
        // 3)
        p1 = head;
        Node newHead = head.next;
        while(p1 != null) {
            Node p2 = p1.next;
            p1.next = p2.next;
            if(p2.next != null) {
                p2.next = p2.next.next;
            }
            p1 = p1.next;
        }
        return newHead;
    }
}

扩展:

    深拷贝 & 浅拷贝:

        浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化。

        深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值