数据结构学习笔记:链表及常见面试题

链表基本结构

public class ListNode {
    // 当前节点值
    int val;
    // 所指向的下一节点
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}

// 构建基本链表
int[] arr = {1, 2, 3, 4, 5};
ListNode head = new ListNode(arr[0]);
ListNode tail = head;

for (int i = 1; i < arr.length; i++) {
    tail.next = new ListNode(arr[i]);
    tail = tail.next;
}

问题关键点

  1. 链表调整函数的返回值类型,往往是节点类型(注意头节点)
  2. 处理链表的过程中,先采用画图的方式理清逻辑
  3. 周围环境需要预先保存(前一节点和后一节点)
  4. 注意边界

1. 环形链表插值

有一个整数 val,如何在节点值有序的环形链表中插入一个节点值为 val 的节点,并且保证这个环形单链表依然有序。

给定链表的信息,及元素的值 A 及对应的 nxt 指向的元素编号同时给定 val,请构造出这个环形链表,并插入该值。

思路

时间复杂度 O(N)、额外空间复杂度 O(1)

令 pre 节点和 cur 节点同步移动,若插入值满足大于 now 节点且小于 pre 节点则将其插入。

注意考虑头尾边界。

实现

public ListNode insert(int[] arr, int[] nxt, int val) {
    if (arr.length == 0) {
        return new ListNode(val);
    }

    ListNode head = new ListNode(arr[0]);
    ListNode tail = head;
    ListNode ins = new ListNode(val);

    // 构建链表
    for (int i = 0; i < arr.length - 1; i++) {
        tail.next = new ListNode(arr[nxt[i]]);
        tail = tail.next;
    }

    // 插入值
    // 若插入值小于头
    if (val < head.val) {
        ins.next = head;
        return ins;
    }

    ListNode pre = head;
    ListNode now = head.next;
    while (now != null && val > now.val) {
        pre = now;
        now = now.next;
    }
    pre.next = ins;
    ins.next = now;

    return head;
}

2. 访问单个节点的删除

实现一个算法,删除单向链表中间的某个结点,假定你只能访问该结点。
思路:复制后一个结点的值给当前结点,并将后一个结点删除
问题:复制时可能存在问题(非整型结点情况)、简单复制会影响外部依赖、尾结点处理问题等。
没有较好的解决方法。

下题则为简单删除:
给定带删除的头节点和要删除的数字,请执行删除操作,返回删除后的头结点。链表中没有重复数字

思路

顺序遍历并删除即可。

实现

public ListNode removeNode(ListNode pNode, int delVal) {
    if (pNode == null) return null;
    if (pNode.val == delVal) return pNode.next;

    ListNode pre = pNode;
    ListNode cur = pNode.next;
    while (cur != null) {
        if (cur.val == delVal) {
            pre.next = cur.next;
            break;
        }
        pre = cur;
        cur = cur.next;
    }
    return pNode;
}

3. 链表的分化

对于一个链表,我们需要用一个特定阈值完成对它的分化,使得小于等于这个值的结点移到前面,大于该值的结点在后面,同时保证两类结点内部的位置关系不变。

给定一个链表的头结点 head,同时给定阈值 val,请返回一个链表,使小于等于它的结点在前,大于等于它的在后,保证结点值不重复。

思路

简单做法:转化为数组完成,类似荷兰国旗问题,再构建链表。

最优解:遍历过程中把所有节点分为两个小链表,最后组合起来即可。

实现

// 题意要求解法 小于等于在前 大于在后
public ListNode listDivide0(ListNode head, int val) {
    if (head == null || head.next == null) return head;

    // 为了保证 minHead 与 min 起始相同,先弄一个临时结点 0
    ListNode cur = head, min = new ListNode(0), minHead = min,
            max = new ListNode(0), maxHead = max;
    while(cur != null) {
        if (cur.val <= val) {
            min.next = cur;
            min = cur;
        } else {
            max.next = cur;
            max = cur;
        }
        cur = cur.next;
    }
    // 链接大小节点
    min.next = maxHead.next;
    max.next = null;

    head = minHead.next;

    return head;
}

4. 打印两个链表的公共值

现有两个升序链表,且链表中均无重复元素。请设计一个高效的算法,打印两个链表的公共值部分。

给定两个链表的头指针 headA 和 headB,请返回一个 vector,元素为两个链表的公共部分。
请保证返回数组的升序。两个链表的元素个数均小于等于 500。保证一定有公共值

思路

链表都不为空时从两个链表的头节点开始遍历:

  1. 若相等则打印节点值并均后移节点
  2. 若不等则后移比较小的那个节点

两者其中一个为空时停止遍历。

实现

public int[] findCommonParts(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    ArrayList<Integer> res = new ArrayList<>();

    while (headA != null && headB != null) {
        if (headA.val == headB.val) {
            res.add(headA.val);
            headA = headA.next;
            headB = headB.next;
        } else if (headA.val > headB.val){
            headB = headB.next;
        } else headA = headA.next;
    }

    int[] finalRes = new int[res.size()];
    for (int i = 0; i < res.size(); i++) {
        finalRes[i] = res.get(i);
    }

    return finalRes;
}

5. 链表的 k 逆序

有一个单链表,请设计一个算法,使得每 K 个节点之间逆序,如果最后不够 K 个节点一组,则不调整最后几个节点。
例如链表 1->2->3->4->5->6->7->8->null,K=3 这个例子。
调整后为,3->2->1->6->5->4->7->8->null。
因为 K==3,所以每三个节点之间逆序,但其中的 7,8 不调整,因为只有两个节点不够一组。

给定一个单链表的头指针 head, 同时给定 K 值,返回逆序后的链表的头指针。

思路

方法一:利用栈实现,逐一压入节点,若栈中存在 k 个节点则弹出。

问题:最后不足 k 个的节点处理需要注意。

方法二:直接在每收集了 k 个节点时进行部分链表逆序

实现

// 方法二的实现
public ListNode inverse1(ListNode head, int k) {
    if (k < 2) {
        return head;
    }
    //当前位置
    ListNode cur = head;
    //开始位置
    ListNode start = null;
    //开始的前一个
    ListNode pre = null;
    //end 的后一个
    ListNode next = null;
    int count = 1;
    while (cur != null) {
        // 获取当前的下一个
        next = cur.next;
        // 如果 count 为 k 执行逆序操作
        if (count == k) {
            // 判断 pre 是否为空,为空 start = head 不为空 start = pre 的下一个节点
            start = pre == null ? head : pre.next;
            // 判断 pre 是否为空 为空 head 变为当前节点(逆序后当前 cur 为 head 了) 不为空 
            // head = head 只进行一次判断
            head = pre == null ? cur : head;
            // 传入前一个节点(起到连接的作用) 首节点 当前节点,当前节点的下一个节点
            resign(pre, start, cur, next);
            pre = start; // start (第一个值赋给 pre )
            count = 0;
        }
        count++;
        cur = next;
    }
    return head;
}

// 部分逆序
public void resign(ListNode left, ListNode start, ListNode end, ListNode right) {
    ListNode pre = start;
    ListNode cur = start.next;
    ListNode next;
    // 逆序开始
    // 保留首节点 pre cur ( pre 下一个节点 要转换的 即 cur.next = pre ) 和 next 节点
    while (cur != right) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    // 如果不是空
    if (left != null) {
        left.next = end;
    }
    start.next = right; // 可以是 right 或者 next
}

6. 链表指定值清除

现在有一个单链表。链表中每个节点保存一个整数,再给定一个值 val,把所有等于 val 的节点删掉。

给定一个单链表的头结点 head,同时给定一个值 val,请返回清除后的链表的头结点,保证链表中有不等于该值的其它值。请保证其他元素的相对顺序。

思路

构建新节点,仅保留与指定值不相等的节点。

注意边界条件。

实现

public ListNode clear(ListNode head, int val) {
    ListNode cur = head;
    ListNode newHead = null, newTail = null;

    while (cur != null) {
        if (cur.val != val) {
            if (newHead == null) {
                newHead = cur;
                newTail = newHead;
            } else {
                newTail.next = cur;
                newTail = newTail.next;
            }
        }
        cur = cur.next;
    }

    if (newTail != null)
        newTail.next = null;
    return newHead;
}

7. 链表的回文结构

请编写一个函数,检查链表是否为回文。

给定一个链表 ListNode pHead,请返回一个 boolean,代表链表是否为回文。
测试样例:

{1,2,3,2,1} 返回:true
{1,2,3,2,3} 返回:false

思路

方法一:将前一半的节点压入栈中,并将当前节点继续遍历,每遍历一个都与栈弹出的节点相比较,若不同则不是。额外空间复杂度 O(N/2)。

方法二:将链表后半部分逆序,从链表两端开始往中间遍历比较,若不同则不是。不需要额外空间。

方法二需要注意比较完毕时将链表结构调整回原始未逆序结构。

实现

// 方法一:一半入栈
public boolean isPalindrome(ListNode head) {
    Stack<Integer> stack = new Stack<>();
    ListNode fastCur = head;
    ListNode slowCur = head;
    int flag = 0;

    // 判断奇偶
    while (slowCur != null) {
        slowCur = slowCur.next;
        flag++;
    }

    slowCur = head;
    while (fastCur != null && fastCur.next != null) {
        stack.push(slowCur.val);
        slowCur = slowCur.next;
        fastCur = fastCur.next.next;
    }

    // 判断最中间是否入栈
    slowCur = flag % 2 == 0 ? slowCur : slowCur.next;

    while (slowCur != null) {
        if (slowCur.val != stack.pop())
            return false;
        slowCur = slowCur.next;
    }
    return true;
}

// 方法二
public boolean isPalindrome1(ListNode head) {
    ListNode cur = head;
    ListNode fastCur = head;
    ListNode slowCur = head;
    int flag = 0;

    // 判断奇偶
    while (cur != null) {
        cur = cur.next;
        flag++;
    }

    // slow 到达中部 fast 到达末尾
    while (fastCur != null && fastCur.next != null) {
        slowCur = slowCur.next;
        fastCur = fastCur.next.next;
    }

    ListNode pre = slowCur;
    ListNode next;
    // 若是奇数正中间那个则跳过比较
    slowCur = flag % 2 == 0 ? slowCur : slowCur.next;
    cur = slowCur;

    // 将链表后半截逆序指向中间
    while (cur != null) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }

    // 中间断开
    if (slowCur != null)
        slowCur.next = null;

    // 从两端开始往中间比较
    ListNode leftCur = head;
    ListNode rightCur = pre;

    // 判断是否回文
    while (leftCur != null && rightCur != null) {
        if (leftCur.val != rightCur.val) {
            return false;
        }
        leftCur = leftCur.next;
        rightCur = rightCur.next;
    }

    cur = pre;
    pre = null;
    // 恢复链表结构
    while (cur != null) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }

    while (head != null) {
        System.out.println(head.val);
        head = head.next;
    }

    return true;
}

8. 复杂链表的复制

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

class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}

// next: A -> B -> C
// random: A -> C, B -> A, C -> B.

思路

  1. 第一次遍历:将复制的节点 A’ 插入到 A 与 B 之间,其后的节点也按照这个方式进行复制。
  2. 再次遍历:通过 A 的 random 指针找到 C,再从 C 的 random 指针找到 C’,从而将 C’ 赋值给 A‘ 的 random 指针,后续节点操作同上。
  3. 分割复制的链表与原链表。

实现

public RandomListNode Clone(RandomListNode head) {
    if (head == null) {
        return null;
    }

    RandomListNode cur = head;
    RandomListNode next;

    // 复制所有结点
    while (cur != null) {
        next = cur.next;
        cur.next = new RandomListNode(cur.label);
        cur.next.next = next;
        cur = next;
    }

    cur = head;
    RandomListNode copyCur;

    // 给复制的结点 random 指针赋值
    while (cur != null) {
        next = cur.next.next;
        copyCur = cur.next;
        copyCur.random = cur.random != null ? cur.random.next : null;
        cur = next;
    }

    RandomListNode res = head.next;
    cur = head;

    // 将复制的链表与原链表分割
    while (cur != null) {
        next = cur.next.next;
        copyCur = cur.next;
        cur.next = next;
        copyCur.next = next != null ? next.next : null;
        cur = next;
    }
    return res;
}

9. 链表判环

如何判断一个单链表是否有环?
有环的话返回进入环的第一个节点的值,无环的话返回 - 1。
如果链表的长度为 N,请做到时间复杂度 O (N),额外空间复杂度 O (1)。

给定一个单链表的头结点 head,请返回所求值。

思路

普通解法:若没有额外空间复杂度限制则可以使用哈希表实现,若后续节点在表中出现过则有环。

最优解:

  1. 从头节点开始使用快慢两个指针进行遍历
  2. 若快指针到达 null 则无环
  3. 若快指针与慢指针相遇,则有环。此时使快指针回到头节点并以一次一步的速度与慢指针同时出发,再次相遇的节点则为入环点。

实现

public int chkLoop(ListNode head) {
    if(head == null || head.next == null) return -1;

    ListNode slowNode = head.next;
    ListNode fastNode = head.next.next;

    // 检验是否存在环
    while (fastNode != slowNode) {
        if (slowNode == null) return -1;
        slowNode = slowNode.next;
        if (fastNode.next == null) return -1;
        fastNode = fastNode.next.next;
    }

    // 两者再次相遇的地方即为环的入口
    fastNode = head;
    while (fastNode != slowNode) {
        slowNode = slowNode.next;
        fastNode = fastNode.next;
    }

    return slowNode.val;
}

10. 无环单链表判相交

现在有两个无环单链表,若两个链表的长度分别为 m 和 n,设计一个时间复杂度为 O (n + m),额外空间复杂度为 O (1) 的算法,判断这两个链表是否相交。

给定两个链表的头结点 headA 和 headB,请返回一个 bool 值,代表这两个链表是否相交。保证两个链表长度小于等于 500。

思路

注意:

(1)一旦两个链表相交,那么两个链表中的节点一定有相同地址。
(2)一旦两个链表相交,那么两个链表从相交节点开始到尾节点一定都是相同的节点。
首先搞清楚什么叫两个链表相交,相交的意思是 从某个节点开始,两个链表的节点重合
一直到结束。
所有长链表的前面多出来的部分可以不用比较 直接遍历跳过。

解法一:

  1. 首先遍历两个指针得到两者的长度
  2. 两者回到头节点,偏长的那个指针先遍历偏长的长度,两者再一起遍历,什么时候相同就是相交。

解法二:可以直接判断最后两个指针是否相同

实现

// 解法一
public boolean chkIntersect(ListNode headA, ListNode headB) {
    int lengthA = 0, lengthB = 0;
    ListNode curA = headA;
    ListNode curB = headB;

    while (curA != null) {
        lengthA ++;
        curA = curA.next;
    }

    while (curB != null) {
        lengthB ++;
        curB = curB.next;
    }

    curA = headA;
    curB = headB;
    boolean flag = lengthA > lengthB;
    int steps = Math.abs(lengthA - lengthB);

    if (flag) {
        while (steps-- > 0) {
            curA = curA.next;
        }
    } else  {
        while (steps-- > 0) {
            curB = curB.next;
        }
    }

    while (curA != null) {
        if (curA == curB) {
            return true;
        }
        curA = curA.next;
        curB = curB.next;
    }

    return false;
}

// 解法二,可以直接判断最后两个指针是否相同
public boolean chkIntersect1(ListNode headA, ListNode headB) {
    if (headA == null || headB == null)
        return false;
    ListNode curA = headA;
    ListNode curB = headB;

    while (curA.next != null) {
        curA = curA.next;
    }

    while (curB.next != null) {
        curB = curB.next;
    }

    return curA == curB;
}

11. 有环单链表相交判断

如何判断两个有环单链表是否相交?相交的话返回第一个相交的节点,不相交的话返回空。
如果两个链表长度分别为 N 和 M,请做到时间复杂度 O (N+M),额外空间复杂度 O (1)。

给定两个链表的头结点 head1 和 head2。
请返回一个 bool 值代表它们是否相交。

思路

  1. 若入环点相同则相交
  2. 若入环点不同则遍历其中一个环,环内遍历时若与另一个入环点相遇则相交。类似于 α \alpha α 的两个小触角为入环点的情况。

实现

public boolean chkInter(ListNode head1, ListNode head2) {
    if(head1 == null || head2 == null) return false;
    // 入环点
    ListNode loopGate1 = chkLoop(head1);
    ListNode loopGate2 = chkLoop(head2);
    ListNode cur;
    if (loopGate1 == loopGate2) {
        // 环入口结点已相交
        return true;
    } else {
        // 找环内的结点
        cur = loopGate1.next;
        while (cur != loopGate1) {
            if (cur == loopGate2) return true;
            cur = cur.next;
        }
    }

    return false;
}

你可能感兴趣的:(算法)