每日算法总结——判断链表是否有环、链表相交问题、二叉树递归/非递归遍历

一、判断链表是否有环

单链表只有一个next指针,所以有环一定是下面的情况(所以不要脑补其他结构了😋):

在这里插入图片描述

方法一、使用HashSet
  • 思路:遍历链表,并依次把节点放入HashSet中,放入前判断节点在Set中是否出现过,由此得到链表是否包含环。
方法二、使用快慢指针(比较魔性🤯)
  • 思路:使用快慢指针遍历链表,快指针一旦走到null,则该单链表一定无环(快指针一次走两步),如果存在环的话,快指针与慢指针一定会在环上相遇
    当快慢指针相遇时,让快指针重新指向初始节点慢指针原地不变;两个指针都每次走一步一定会在链表的入环节点相遇
  • 利用数学方法可以证明,实际做题记住结论就行。
public static Node getLoopNode(Node head) {
    if (head == null || head.next == null || head.next.next == null) {
        return null;
    }
    // 初始化快慢指针
    Node slow = head.next;
    Node fast = head.next.next;
    while (slow != fast) {
        if (fast.next == null || fast.next.next == null) {
            return null;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    // 快指针返回链表初始节点
    fast = head;
    while (fast != slow) {
        fast = fast.next;
        slow = slow.next;
    }
    return fast;
}

二、链表相交问题

链表相交的情况有多种,需要分开讨论

情况一、两个链表都无环
  • 思路:首先对两个链表进行遍历,获得最后一个节点,如果两个链表得**最后一个节点不相同的话,说明两个链表不为空(相交只可能是Y字形);
    在遍历的过程中同时获取两个链表的长度,两个链表的
    公共部分**长度一定相同,所以需要保证非公共部分长度一致:对长链表遍历,遍历的次数为两链表长度之差(切除过长部分);
    之后两链表同时遍历,当两指针指向的节点相同时,返回该节点。
public static Node noLoop(Node head1, Node head2) {
    // 先遍历到最后一个节点,因为两个链表最后一个节点不相同的话,一定不相交
    // 同时获得两个链表各自的长度
    Node cur1 = head1, cur2 = head2;
    int n = 0;
    while (cur1.next != null) {
        cur1 = cur1.next;
        n++;
    }
    while (cur2.next != null) {
        cur2 = cur2.next;
        n--;
    }
    if (cur1 != cur2) {
        return null;
    }
    // cur1变成较长链表的头,cur2变成较短链表的头
    cur1 = n > 0 ? head1 : head2;
    cur2 = cur1 == head1 ? head2 : head1;
    n = Math.abs(n);
    while (n > 0) {
        cur1 = cur1.next;
        n--;
    }
    while (cur1 != cur2) {
        cur1 = cur1.next;
        cur2 = cur1.next;
    }
    return cur1;
}
情况二、两个链表一个有环、一个无环

不可能存在的结构

情况三、两个链表都有环
  • 思路判断两个链表的入环节点是否为同一个
    • 若入环节点为同一个,则相交节点在入环节点之前(如图二),此时可将入环节点看作两个链表的终止节点,然后进行情况一的判断
    • 若入环节点不为同一个,则设定两个指针p1,p2分别指向两个链表的入环节点,保持p1不变,p2继续向下走:
      • p2指向了p1所指的节点(如图三),说明两个链表的入环节点不同,但环相同,此时返回两个入环节点之一
      • p2回到了起点(即入环节点),则说明两链表不相交,如图1。

在这里插入图片描述

public class FindFirstIntersectNode {
    /**
     * 【题目】给定两个可能有环也可能无环的单链表,头节点head1和head2。
     * 请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null
     * 【要求】如果两个链表长度之和为N,时间复杂度请达到$O(N)$,额外空间复杂度请达到$O(1)$。
     */
    public static class Node {
        public int value;
        public Node next;

        public Node(int data) {
            this.value = data;
        }
    }

    public static Node getIntersectNode(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return null;
        }
        Node loop1 = getLoopNode(head1);
        Node loop2 = getLoopNode(head2);
        if (loop1 == null && loop2 == null) {
            return noLoop(head1, head2);
        } else {
            return bothLoop(head1, loop1, head2, loop2);
        }
    }

    /**
     * 找到链表的第一个入环节点,如果没有环,则返回空
     * 使用快慢指针
     */
    public static Node getLoopNode(Node head) {
        if (head == null || head.next == null || head.next.next == null) {
            return null;
        }
        // 初始化快慢指针
        Node slow = head.next;
        Node fast = head.next.next;
        while (slow != fast) {
            if (fast.next == null || fast.next.next == null) {
                return null;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        // 快指针返回链表初始节点
        fast = head;
        while (fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }

    /**
     * 如果两个链表都无环,返回第一个相交节点,如果不相交,返回null
     */
    public static Node noLoop(Node head1, Node head2) {
        // 先遍历到最后一个节点,因为两个链表最后一个节点不相同的话,一定不相交
        // 同时获得两个链表各自的长度
        Node cur1 = head1, cur2 = head2;
        int n = 0;
        while (cur1.next != null) {
            cur1 = cur1.next;
            n++;
        }
        while (cur2.next != null) {
            cur2 = cur2.next;
            n--;
        }
        if (cur1 != cur2) {
            return null;
        }
        // cur1变成较长链表的头,cur2变成较短链表的头
        cur1 = n > 0 ? head1 : head2;
        cur2 = cur1 == head1 ? head2 : head1;
        n = Math.abs(n);
        while (n > 0) {
            cur1 = cur1.next;
            n--;
        }
        while (cur1 != cur2) {
            cur1 = cur1.next;
            cur2 = cur1.next;
        }
        return cur1;
    }

    /**
     * 两个有环链表,返回第一个相交节点,如果不相交返回null
     *
     * @param head1 链表1的头节点
     * @param loop1 链表1的入环节点
     * @param head2 链表2的头节点
     * @param loop2 链表2的入环节点
     * @return 相交节点
     */
    public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
        Node cur1 = null, cur2 = null;
        if (loop1 == loop2) {
            cur1 = head1;
            cur2 = head2;
            int n = 0;
            while (cur1 != loop1) {
                n++;
                cur1 = cur1.next;
            }
            while (cur2 != loop2) {
                n--;
                cur2 = cur2.next;
            }
            cur1 = n > 0 ? head1 : head2;
            cur2 = cur1 == head1 ? head2 : head1;
            n = Math.abs(n);
            while (n != 0) {
                cur1 = cur1.next;
                n--;
            }
            while (cur1 != cur2) {
                cur1 = cur1.next;
                cur2 = cur2.next;
            }
            return cur1;
        } else {
            cur1 = loop1.next;
            cur2 = loop2;
            while (cur1 != loop1) {
                if (cur1 == cur2) {
                    return loop1;
                }
                cur1 = cur1.next;
            }
            return null;
        }
    }
}

三、二叉树递归/非递归遍历

1、递归

递归遍历方法不难,代码也很好写,三者的区别在于输出的顺序不同

递归先序遍历
public static void preOrderRecur(Node head) {
    if (head == null) {
        return;
    }
    System.out.print(head.value + " ");
    preOrderRecur(head.left);
    preOrderRecur(head.right);
}
递归中序遍历
public static void inOrderRecur(Node head) {
    if (head == null) {
        return;
    }
    inOrderRecur(head.left);
    System.out.print(head.value + " ");
    inOrderRecur(head.right);
}
递归后序遍历
public static void posOrderRecur(Node head) {
    if (head == null) {
        return;
    }
    posOrderRecur(head.left);
    posOrderRecur(head.right);
    System.out.print(head.value + " ");
}
2、非递归

递归方式本质上是系统帮我们维护一个栈,并针对我们的递归操作进行弹栈/压栈处理;而非递归的方法就是由程序员手动弹栈/压栈

非递归先序遍历

算法步骤

  1. 从栈中弹出一个节点cur

  2. 对该节点的value进行打印/处理

  3. cur节点的孩子按照先左后右的顺序依次压入栈(如果有的话)

  4. 重复1~3

/**
* 非递归先序遍历
*/
public static void preOrderUnRecur(Node head) {
   System.out.print("pre-Order: ");
   if (head != null) {
       Stack<Node> stack = new Stack<>();
       stack.push(head);
       Node cur = null;
       while (!stack.isEmpty()) {
           cur = stack.pop();
           System.out.print(cur.value + " ");
           if (cur.left != null) {
               stack.push(cur.left);
           }
           if (cur.right != null) {
               stack.push(cur.right);
           }
       }
   }
   System.out.println();
}
非递归中序遍历

算法思想:对于每棵子树,整棵树左边界进栈,即不断将left放入栈中,直到leftnull,然后将栈顶节点弹出打印,并将弹出节点的right子树(如果有的话)重复上述左边界进栈,直到栈为空

/**
 * 非递归中序遍历
 */
public static void inOrderUnRecur(Node head) {
    System.out.print("in-Order: ");
    if (head != null) {
        Stack<Node> stack = new Stack<>();
        while (!stack.isEmpty() || head != null) {
            if (head != null) {
                stack.push(head);
                head = head.left;
            } else {
                head = stack.pop();
                System.out.println(head.value + " ");
                head = head.right;
            }
        }
    }
    System.out.println();
}

ps:确实抽象🤯

非递归后序遍历

算法思想:上述先序遍历打印的顺序是父左右,可以将对左右孩子的处理顺序调换,这样打印的顺序是父右左,可以发现这个顺序的逆序正是后序遍历的顺序,所以可以把打印操作换成压栈操作,即再准备一个收集栈,用于调转打印顺序。

/**
* 非递归后序遍历
*/
public static void posOrderUnRecur(Node head) {
   System.out.println("pos-Order: ");
   if (head != null) {
       Stack<Node> stack1 = new Stack<>();
       Stack<Node> stack2 = new Stack<>();
       stack1.push(head);
       while (!stack1.isEmpty()) {
           head = stack1.pop();
           stack2.push(head);
           if (head.left != null) {
               stack2.push(head.left);
           }
           if (head.right != null) {
               stack2.push(head.right);
           }
       }
       while (!stack2.isEmpty()) {
           System.out.print(stack2.pop().value + " ");
       }
   }
   System.out.println();
}

二叉树遍历属于必须要熟练掌握的内容,其中非递归遍历可能会在面试场合出现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值