剑指offer_面试题23:链表中环的入口节点(交叉链表)、面试题25:合并两个有序链表(合并k个有序链表)、面试题26:树的子结构(树的子树)

面试题23:链表中环的入口节点

在这里插入图片描述
在这里插入图片描述

① 使用哈希表(用时6ms)
  • 将不重复的非空节点存入哈希表,第一个重复的节点就是环的入口节点。
  • 代码如下:
public ListNode detectCycle(ListNode head) {
    
    
    HashMap<ListNode, Integer> map = new HashMap<>();
    while (head != null) {
    
    
        if (map.containsKey(head)) {
    
    // 第一个重复的节点,环的入口
            return head;
        }
        map.put(head, 1);
        head = head.next;
    }
    return null;//没有环,返回null
}
② 快慢指针(竟然用时0ms)
  • 思路分析:
  1. 判断链表中是否有环: 通过快慢指针,如果链表中有环,则快慢指针会在非null节点相遇;否则,快指针到达null节点,慢指针没有与快指针相遇。
  2. 获取环的长度: 固定快指针不动,慢指针移动n步再次与快指针相遇,则环的长度为n
  3. 快慢指针都回到链表头节点,快指针先走n步。然后快慢指针一起走,每次只走一步,最后相遇的节点就是环的入口节点。
    在这里插入图片描述
  • 代码如下:
public ListNode detectCycle(ListNode head) {
    
    
    if (head == null) {
    
    
        return null;
    }
    // 利用快慢指针,先判断链表是否有环
    ListNode fast = head.next, slow = head;
    boolean cycled = true;
    while (slow != fast) {
    
    // 如果slow等于fast说明有环
        if (fast == null || fast.next == null) {
    
    
            cycled = false;
            break;
        }
        fast = fast.next.next;
        slow = slow.next;
    }
    if (!cycled) {
    
    
        return null;
    }
    // 有环,求环的长度
    int len = 1;
    slow = slow.next;
    while (slow != fast) {
    
    
        len++;
        slow = slow.next;
    }
    // 都回到头节点,查找入口节点
    fast = head;
    slow = head;
    // 快指针先后len步
    while (len > 0) {
    
    
        fast = fast.next;
        len--;
    }
    // 快慢指针一起走
    while (slow != fast) {
    
    
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}
变形 —— leetcode:环形链表I
  • 求链表的入口节点中,通过快慢指针判断链表是否有环。
  • 代码如下:
public boolean hasCycle(ListNode head) {
    
    
    if (head == null) {
    
    
        return false;
    }
    ListNode fast = head.next, slow = head;
    while (slow != fast) {
    
    // slow和fast指向一样,说明存在环
        if (fast == null || fast.next == null) {
    
    // 遇到null节点,说明不是环形链表
            return false;
        }
        fast = fast.next.next;
        slow = slow.next;
    }
    return true;
}
变形 —— leetcode160:相交链表

在这里插入图片描述

  • 两次遍历:
  1. 第一次遍历,分别获取每个链表的长度。
  2. 让较长链表的指针先走m - n步,这时两个链表的指针同时移动。如果存在交叉,则会在交叉点相遇;否则,都会到达各自的null节点。
  • 两次遍历的代码:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    
    
    int len1 = 0, len2 = 0;
    // 获取链表A和链表B的长度
    ListNode p = headA, q = headB;
    while (p != null) {
    
    
        len1++;
        p = p.next;
    }
    while (q != null) {
    
    
        len2++;
        q = q.next;
    }
    // 长的链表先走
    p = headA;
    q = headB;
    if (len1 > len2) {
    
    
        int diff = len1 - len2;
        while (diff > 0) {
    
    
            p = p.next;
            diff--;
        }
    } else {
    
    
        int diff = len2 - len1;
        while (diff > 0) {
    
    
            q = q.next;
            diff--;
        }
    }
    // 两个链表同时走
    while (p != q) {
    
    
        p = p.next;
        q = q.next;
    }
    return p;
}
  • 走两条链表: 每条链表的指针从自身头节点开始走到null节点,然后接着走另一条链表。如果存在交叉,则在交叉节点相遇;否则,各自走到null节点。
  • 特殊情况: 在没有切换到另外一条链表前,就出现了指向相同节点的情况。要么头节点到交叉节点的长度一致,要么没有交叉且头节点到尾结点的长度一致。
  • 代码如下:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    
    
    ListNode p = headA, q = headB;
    while (p != q) {
    
    
        p = (p != null) ? p.next : headB;
        q = (q != null) ? q.next : headA;
    }
    return p;
}

面试题24:反转链表

在这里插入图片描述

  • 思路:
  1. 创建为null的prev节点,指想新链表的头节点。
  2. 然后用cur指针指向原链表的头节点,依次遍历链表中的节点;将cur指向的节点的next指向prev,并更新prev。
  3. 为了避免cur节点的next节点断开,需要创建临时指针指向cur的next节点,完成当前节点的转置后,更新cur指针。
  • 注意: 由于while循环的条件是cur != null,并且prev初始化为null,因此不需要检测head是否为null,可以直接将head作为cur指针。
  • 代码如下:
public ListNode reverseList(ListNode head) {
    
    
    ListNode prev = null;
    while (head != null) {
    
    
        ListNode temp = head.next;
        head.next = prev;
        prev = head;
        head = temp;
    }
    return prev;
}

面试题25:合并两个有序链表

在这里插入图片描述

① 迭代
  • 头部对齐,谁的节点值更小,谁先加入合并后的链表中。
  • 特殊情况:
  1. 如果某一个为null,合并后的链表就是另外一个链表,这时可以直接返回。
  2. 就算两个链表一样长,也会出现有个链表有剩余的情况,不需要单独添加null节点,直接将剩余的链表添加到合并聊表的尾部即可。
  • 代码如下,由于代码的设计,我们不需要处理null的特殊情况,因为head.next默认是null。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
    
    ListNode head = new ListNode(0);
    ListNode p = head;// 指向最新的节点,防止head更改指向
    while (l1 != null && l2 != null) {
    
    // 头部对齐,合并链表
        if (l1.val < l2.val) {
    
    
            p.next = l1;
            l1 = l1.next;
        } else {
    
    
            p.next = l2;
            l2 = l2.next;
        }
        p = p.next;
    }
    // 链表有剩余,就算是两个链表一样长,也会有个链表有剩余
    if (l1 != null) {
    
    
        p.next = l1;
    }
    if (l2 != null) {
    
    
        p.next = l2;
    }
    return head.next;
}
② 递归
  • 每次传入两个链表,如果一个null,直接返回另一个;否则根据值的大小,插入新的节点,递归进行下一次合并。
  • 直接将当前较小值的指针作为新的头节点,头节点的next是递归合并的结果,返回该头节点。
  • 代码如下:
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;
    }
}
③ 变形 —— leetcode23:k个有序链表的合并
  • 按列比较,不借助优先级队列:
  1. 每次都遍历所有链表,按列比较找到具有最小值的链表下标。
  2. 如果所有的链表都为null,则停止按列比较。
  • 代码如下,花费352ms。时间复杂度 O ( m n ) O(mn) O(mn)n表示链表个数,m表示最长的链表长度。
public ListNode mergeKLists(ListNode[] lists) {
    
    
    ListNode head = new ListNode(0);
    ListNode p = head;
    /*if(lists.length == 1){
    	return lists[0];
    }*/
    while (true) {
    
    
        boolean flag = true; // 是否所有链表为null
        int min = Integer.MAX_VALUE;// 记录按列比较中的最小值节点
        int min_index = 0;
        for (int i = 0; i < lists.length; i++) {
    
    
            if (lists[i] != null) {
    
    
                if (lists[i].val < min) {
    
    
                    min = lists[i].val;
                    min_index = i;
                }
                flag = false;// 存在非null链表
            }
        }
        if (flag) {
    
    // 所有链表为null,停止按列比较
            break;
        }
        // 存在最小值节点,更新相关指针
        p.next = lists[min_index];
        p = p.next;
        lists[min_index] = lists[min_index].next;
    }
    // 其实可以不用指定,因为最后加入的节点的next指针一定是null
    // 就算没有任何节点加入,head的next默认为null
    p.next = null;
    return head.next;
}
  • 效率优化: 如果只有一个链表不用通过按列比较合并,直接返回结果即可,只减少了12ms
  • 借助优先级队列,实现自动比较节点大小,每次获取队列中的头节点即可。
  • 代码如下,运行时间6ms。
public ListNode mergeKLists(ListNode[] lists) {
    
    
    ListNode head = new ListNode(0);
    ListNode p = head;
    PriorityQueue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
    
    
        @Override
        public int compare(ListNode o1, ListNode o2) {
    
    
            return o1.val - o2.val;
        }
    });
    for (int i = 0; i < lists.length; i++) {
    
    // 预先加入第一列的节点
        if (lists[i] != null) {
    
    
            queue.add(lists[i]);
        }
    }
    while (!queue.isEmpty()) {
    
    
        ListNode temp = queue.poll();
        p.next = temp;
        p = p.next;
        // 只要被加入节点的next不为null,更新链表的指向
        if (temp.next != null) {
    
    
            queue.add(temp.next);
        }
    }
    // 其实可以不用指定,因为最后加入的节点的next指针一定是null
    // 就算没有任何节点加入,head的next默认为null
    p.next = null;
    return head.next;
}

面试题26:树的子结构

在这里插入图片描述

  • 根据题意,子结构并不是子树,子结构的要求更低,只要是原树的某部分即可。
    在这里插入图片描述
  • 上图中,右边的树是左边树的子结构,但不是子树!
  • 思路:
  1. 递归判断是够存在子结构,如果其中一棵树的root为null,说明不存在子结构;否则,递归检查当前root的子结构是否一致;
  2. 如果当前root的子结构不一致,则检查左子树的子结构;还不一致则检查右子树的子结构。
  3. 判断是否为子结构:判断两个节点的值是否一致且左子树也一致且右子树也一致;如果树B的子节点为null,说明已经检查完毕,直接返回true;如果树A的节点为null,说明树B还有子节点,不是树A的子结构。
  • 注意:
  1. 以root1为基准进行子树判断,可能需要访问root1的左右孩子节点。 如果root1位null,就应该停止比较了
  2. 题目中规定,如果root2位null,不是任何树的子结构,也可以停止比较。
  • 代码:以root1为基准进行子树判断,可能需要访问root1的左右孩子节点。
public boolean HasSubtree(TreeNode root1, TreeNode root2) {
    
    
    if (root1 == null || root2 == null) {
    
    
        return false;
    }
    // 先判断从root开始是否为子结构,如果不是再判断左子树;
    // 还不是,则继续判断右子树;巧用||符号的短路原则解决if ... else条件判断
    return isSubTree(root1, root2) || HasSubtree(root1.left, root2) ||
            HasSubtree(root1.right, root2);
}

public boolean isSubTree(TreeNode root1, TreeNode root2) {
    
    
    if (root2 == null) {
    
    // 树B遍历完成
        return true;
    }
    if (root1 == null) {
    
    // 树A没有子节点了,树B还有
        return false;
    }
    return (root1.val == root2.val) && isSubTree(root1.left, root2.left) &&
            isSubTree(root1.right, root2.right);
}
变形 —— leetcode572:另一个树的子树
  • 该题中st都不为null,根据一个树的子结构:
  1. 先设计一个辅助方法isSame()用于判断当前s的子树t是否相同。
  2. 在原有函数中先调用isSame()判断根节点开始的子树和t是否相同,如果不相同,才递归调用原函数判断s的左子树t是否相同;最后,在判断s的右子树t是否相同。
  3. 因为st都不为null原本以为无需判断st是否为null的情况,但是以s为基准进行递归遍历,必须判断s为null的情况。因为可能调用其左子树或右子树。
  • 代码如下:
public boolean isSubtree(TreeNode s, TreeNode t) {
    
    
    // 虽然s和t都非空,以s为基准递归遍历,可能出现s为null 的情况
    if (s==null){
    
    
        return false;
    }
    // 先判断s和t,不是子树接着判断s.left和t;否则,接着判断s.right和t
    return isSame(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t);
}

public boolean isSame(TreeNode s, TreeNode t) {
    
    // 从当前节点起,递归判断是否为子树
    if (s == null && t == null) {
    
    
        return true;
    }
    if (s == null || t == null) {
    
    
        return false;
    }
    return (s.val == t.val) && isSame(s.left, t.left) && isSame(s.right, t.right);
}

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/100878759
今日推荐