算法通关村-第1关

152 阅读10分钟

青铜🍄

两个链表,找公共子节点

方法:可以将两个链表存到某个数据结构中,如hashMap、hashSet、栈中,然后依次比较

1.hashSet 辅助查找

将其中一个链表存到 hashSet中,利用hashSet.contains( )方法 来比较

/**
 * 通过 hashSet 辅助查找
 * @param headA
 * @param headB
 * @return
 */
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
    Set<ListNode> set = new HashSet();
    // 将链表A 放进 hashSet中
    while (headA != null) {
        set.add(headA);
        headA = headA.next;
    }

    // 遍历链表B,并检测 set是否存在当前结点
    while (headB != null) {
        if (set.contains(headB)) {
            return headB;
        }
        headB = headB.next;
    }
    return null;
}

2.hashMap 辅助查找

new 一个hashMap,HashMap<ListNode, Integer> hashMap = new HashMap<>();

key值为结点,value为null即可。

将其中一个链表存到 hashMap中,利用hashMap.containsKey( )方法 来比较

/**
 * 通过 HashMap 辅助查找
 * @param head1
 * @param head2
 * @return
 */
public static ListNode findFirstCommonNodeByMap(ListNode head1, ListNode head2) {
    HashMap<ListNode, Integer> hashMap = new HashMap<>();
    while (head1 != null) {
        hashMap.put(head1,null);
        head1 = head1.next;
    }

    while (head2 != null) {
        if (hashMap.containsKey(head2)) {
            return head2;
        }
        head2 = head2.next;
    }
    return null;
}

3.通过栈, 辅助查找

栈是先入后出。所以先出的结点是相同的结点。

/**
 * 通过栈 辅助查找
 * @param head1
 * @param head2
 * @return
 */
public static ListNode findFirstCommonNodeByStack(ListNode head1,ListNode head2) {
    Stack<ListNode> stack1 = new Stack();
    Stack<ListNode> stack2 = new Stack();

    while (head1 != null) {
        stack1.push(head1);
        head1 = head1.next;
    }
    while (head2 != null) {
        stack2.push(head2);
        head2 = head2.next;
    }

    ListNode preNode = null;
    while (stack1.size() > 0 && stack2.size() > 0) {
        // 栈是先入后出。所以先出的结点是相同的结点
        if (stack1.peek() == stack2.peek()) {
            preNode = stack1.pop();
            stack2.pop();
        } else {
            break;
        }
    }
    return preNode;
}

🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄

白银🍄

1.两个链表第一个公共子节点

1.1 蛮力法

在第一链表上顺序遍历每个节点,每遍历到一个节点,就在第二个链表上顺序遍历每个节点。如果在第二个链表上有一个节点和第一个链表上的节点一样,则说明两个链表在这个节点上重合,时间复杂度高,排除!!!

1.2 HashMap辅助查找

将一个链表全部存到HashMap里,然后再遍历第二个,如果有交点,那么一定能在访问到某个元素的时候检测出来;这里存放的是HashMap的键,因为HashMap的键是唯一的,不允许重复!!!

/**
     * 通过 hashMap 辅助查找
     * @param head1
     * @param head2
     * @return
     */
public static ListNode findFirstCommonNodeByMap(ListNode head1, ListNode head2) {
    HashMap<ListNode, Integer> hashMap = new HashMap<>();
    while (head1 != null) {
        hashMap.put(head1,null);
        head1 = head1.next;
    }
    while (head2 != null) {
        if (hashMap.containsKey(head2)) {
            return head2;
        }
        head2 = head2.next;
    }
    return null;
}

1.3 HashSet辅助查找

可以用HashMap,也可以用 HashSet试试!

/**
 * 通过 hashSet 辅助查找
 * @param headA
 * @param headB
 * @return
 */
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
    Set<ListNode> set = new HashSet();
    // 将链表A 放进 hashSet中
    while (headA != null) {
        set.add(headA);
        headA = headA.next;
    }

    // 遍历链表B,并检测 set是否存在当前结点
    while (headB != null) {
        if (set.contains(headB)) {
            return headB;
        }
        headB = headB.next;
    }
    return null;
}

1.4 使用栈辅助查找

使用2个栈,将2个链表分别入栈,然后分别出栈(此时出栈的都是相同的节点),如果相等就继续出栈,不相等的时候就找到了分界线了

/**
 * 通过栈 辅助查找
 * @param head1
 * @param head2
 * @return
 */
public static ListNode findFirstCommonNodeByStack(ListNode head1,ListNode head2) {
    Stack<ListNode> stack1 = new Stack();
    Stack<ListNode> stack2 = new Stack();

    while (head1 != null) {
        stack1.push(head1);
        head1 = head1.next;
    }
    while (head2 != null) {
        stack2.push(head2);
        head2 = head2.next;
    }

    ListNode preNode = null;
    while (stack1.size() > 0 && stack2.size() > 0) {
        // 栈是先入后出。所以先出的结点是相同的结点
        if (stack1.peek() == stack2.peek()) {
            preNode = stack1.pop();
            stack2.pop();
        } else {
            break;
        }
    }
    return preNode;
}

1.5 差和双指针

第一次遍历两个链表,算出两个链表的长度以及长链表比短链表多出若干节点?

第二次遍历,长链表先走若干个节点,然后长链表、短链表一起遍历,找到的第一个相同的节点就是它们的第一个公共节点。

/**
 * 差和双指针
 * @param head1
 * @param head2
 * @return
 */
public static ListNode findFirstCommonNode_2(ListNode head1, ListNode head2) {
    int head1Length = getLength(head1);
    int head2Length = getLength(head2);
    int sub = head1Length >= head2Length ? head1Length - head2Length : head2Length - head1Length;

    if (head1Length > head2Length) {
        int k = 0;
        while (k < sub) {
            k++;
            head1 = head1.next;
        }
    }
    if (head2Length > head1Length) {
        int k = 0;
        while (k < sub) {
            k++;
            head2 = head2.next;
        }
    }
    // 同时遍历两个链表
    while (head1 != head2) {
        head1 = head1.next;
        head2 = head2.next;
    }
    return head1;
}

2.判断链表是否为回文序列

2.1 全部压栈

将链表全部入栈,一边出栈,一边与链表比较,如果全部相同,就是回文序列

/**
 * 全部压栈
 * @param head
 * @return
 */
public static boolean isPalindromeByAllStack(ListNode head) {
    Stack<Integer> stack = new Stack<>();
    ListNode cur = head;
    // 全部压栈
    while (cur != null) {
        stack.push(cur.val);
        cur = cur.next;
    }

    // 全部出栈,并与链表进行比较
    while (stack.size() > 0) {
        if (head.val != stack.pop()) {
            return false;
        }
        head = head.next;
    }
    return true;
}

2.2 出栈一半

先遍历第一遍,得到总长度。之后一遍历链表,一遍压栈。当到达链表长度一半的位置之后,就不再压栈,而是一边出栈,一遍遍历,一遍比较,只要有一个不相等,就不是回文链表。

/**
 * 将全部数据压栈,只出栈一半的数据并比较
 *
 * @param head
 * @return
 */
public static boolean isPalindromeByHalfStack(ListNode head) {
    if (head == null) {
        return true;
    }
    Stack<Integer> stack = new Stack<>();
    ListNode cur = head;
    while (cur != null) {
        stack.push(cur.val);
        // 将全部数据压栈
        cur = cur.next;
    }
    // 数据的长度
    int size = stack.size();
    // 数据长度的一半
    int len = size / 2;
    while (len-- > 0) {
        if (head.val != stack.pop()) {
            return false;
        }
        head = head.next;
    }
    return true;
}

2.3 快慢指针法

3. 合并有序链表

3.1 合并两个有序链表

解题思路:一,新建一个新链表,然后分别遍历两个链表,将最小节点链接到新链表上,最后排完;二,建一个链表结点拆下来,逐个合并到另一个链表对应的位置上去。

/**
 * 方法1:面试时就能写出来的方法
 *
 * @param list1
 * @param list2
 * @return
 */
public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    ListNode newHead = new ListNode(-1);
    ListNode res = newHead;
    while (list1 != null && list2 != null) {
        // list1不为null 和 list2不为null 的情况下
        if (list1.val < list2.val) {
            newHead.next = list1;
            list1 = list1.next;
        } else if (list1.val > list2.val) {
            newHead.next = list2;
            list2 = list2.next;
        } else {
            // 相等情况,分别链接两个链表
            newHead.next = list1;
            list1 = list1.next;
            newHead = newHead.next;
            newHead.next = list2;
            list2 = list2.next;
        }
        newHead = newHead.next;
    }

    while (list1 != null) {
        // list1不为null 和 list2为null 的情况下
        newHead.next = list1;
        list1 = list1.next;
        newHead = newHead.next;
    }
    while (list2 != null) {
        // list1为null 和 list2不为null 的情况下
        newHead.next = list2;
        list2 = list2.next;
        newHead = newHead.next;
    }
    return res.next;
}

方法2:在方法1中,发现两个继续优化的点,一个是上面第一个大while里有三种情况,我们可以将其合并成两个,如果两个链表存在相同元素,第一次出现时使用if(l1.val<= 12.val)来处理,后面一次则会被else处理掉,什么意思呢?我们看一个序列。

假如list1为{1,5,8,12},list2为{2,5,9,13},此时都有一个node(5)。当两个链表都到5的位置时,出现list1.val == list2.val,此时list1中的node(5)会被合并进来。然后list1继续向前走到了node(8),此时list2还是node(5),因此就会执行else中的代码块。这样就可以将第一个while的代码从三种变成两种,精简了很多。

第二个优化是后面两个小的while循环,这两个while最多只有一个会执行,而且由于链表只要将链表头接好,后面的自然就接上了。

/**
 * 方法2,方法1的优化
 * @param list1
 * @param list2
 * @return
 */
public static ListNode mergeTwoLists2(ListNode list1, ListNode list2) {
    ListNode preHead = new ListNode(-1);
    ListNode result = preHead;
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            preHead.next = list1;
            list1 = list1.next;
        } else {
            preHead.next = list2;
            list2 = list2.next;
        }
        preHead = preHead.next;
    }
    preHead.next = list1 == null ? list2 : list1;
    return result.next;
}

4. 双指针

双指针就是两个变量,通过双指针来遍历数据结构,可以有效解决许多问题,如排序、查找、合并等。

4.1 寻找链表的中间节点

使用快慢指针,slow 和 fast,slow走一步,fast走两步,fast到达链表结尾,slow则到达链表中间。

public static ListNode middleNode(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

4.2 返回倒数第k个节点

解题思路:快慢指针,快指针先后遍历到 k+1 个节点,慢指针指向头节点,这样快慢指针之间间隔 k 个节点。之后两个指正同步向后走,快指针走到链表尾部的空节点时,慢指针刚好指向链表的倒数第k个节点。

public static ListNode getKthFromEnd(ListNode head, int k) {
    ListNode fast = head;
    ListNode slow = head;

    while (fast != null && k > 0) {
        fast = fast.next;
        k--;
    }
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow.val;
}

4.3 旋转链表

思路:链表向右移动 k 个位置,假设 k = 2,这时候就将链表分为两个部分{1,2,3}和{4,5}。

最终结果是,{4,5}链接{1,2,3},即,4->5->1->2->3。解题思路就是找到这两部分

注意,这里要考虑 k 可能大于链表长度的情况。如果 长度为5,那么 k = 2和 k = 7的情况是一样的,这里可以用取余 %

public static ListNode rotateRight(ListNode head, int k) {
    if (head == null || k == 0) {
        return head;
    }
    ListNode temp = head;
    ListNode fast = head;
    ListNode slow = head;
    int len = 0;
    // 因为 k 可能 大于链表长度,我们先计算链表长度
    while (head != null) {
        len++;
        head = head.next;
    }
    // 当 k % len == 0 时,直接返回原链表即可
    if (k % len == 0) {
        return temp;
    }

    // 1.快指针先走k步
    // 这里使用取模,是为了防止 k > len的情况
    // 假设len=5,那么k=2和k=7,效果是一样的
    while ((k % len) > 0) {
        k--;
        fast = fast.next;
    }

    // 2.快慢指针一起执行
    // 当fast到尾结点时,slow刚好在倒数第k个位置上
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }

    // 将快指针的下一结点指向头节点,将慢指针的下一结点
    ListNode res = slow.next;
    fast.next = temp;
    slow.next = null;
    return res;
}

5. 删除元素

5.1 删除特定结点

遍历链表,符合就移除!

/**
 * 删除特定值的结点
 *
 * @param head
 * @param val
 * @return
 */
public static ListNode removeElements(ListNode head, int val) {
    ListNode dummyHead = new ListNode(0);
    dummyHead.next = head;
    ListNode temp = dummyHead;
    while (temp.next != null) {
        if (temp.next.val == val) {
            temp.next = temp.next.next;
        } else {
            temp = temp.next;
        }
    }
    return dummyHead.next;
}

5.2 删除倒数第 n 个节点

解题思路:找到要删除节点的前一节点,node.next = node.next.next

方法1:双指针

通过快慢指针,拿到{3,4,5},{4}为需要删除的倒数第2个节点,通过slow.next = slow.next.next删除{4}

/**
 * 方法1:通过双指针
 *
 * @param head
 * @param n
 * @return
 */
public static ListNode removeNthFromEndByTwoPoints(ListNode head, int n) {
    ListNode dummyHead = new ListNode(0);
    dummyHead.next = head;
    ListNode first = head;
    ListNode second = dummyHead;
    for (int i = 0; i < n; i++) {
        first = first.next;
    }
    while (first != null) {
        first = first.next;
        second = second.next;
    }
    // 如果first == null,此时 second指向被删除结点的前一结点
    second.next = second.next.next;
    return dummyHead.next;
}

方法2:遍历链表

遍历链表得到链表长度length,通过 length - n + 1 得到要删除节点的前一节点

/**
 * 方法2:删除倒数第N个结点
 *
 * @param head
 * @param n
 * @return
 */
public static ListNode removeNthFromEndByLength(ListNode head, int n) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode cur = dummy;
    int length = getLength(head);
    for (int i = 1; i < length - n + 1; i++) {
        cur = cur.next;
    }
    cur.next = cur.next.next;
    return dummy.next;
}

public static int getLength(ListNode head) {
    int length = 0;
    while (head != null) {
        ++length;
        head = head.next;
    }
    return length;
}

5.3 删除升序链表的重复元素

5.3.1 删除重复元素,仅保留一个

两两比较,如果值相同就删除

/**
 * 删除升序链表的重复元素,仅保留一个
 *
 * @param head
 * @return
 */
public static ListNode deleteDuplicate(ListNode head) {
    if (head == null) {
        return head;
    }
    ListNode cur = head;
    while (cur.next != null) {
        if (cur.val == cur.next.val) {
            cur.next = cur.next.next;
        } else {
            cur = cur.next;
        }
    }
    return head;
}

5.3.2 删除所有重复元素

如果cur.next.val == cur.next.next.val,就删除两个节点 cur.next = cur.next.next;

/**
 * 重复元素都不要
 *
 * @param head
 * @return
 */
public static ListNode deleteDuplicates(ListNode head) {
    if (head == null) {
        return head;
    }

    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode cur = dummy;
    while (cur.next != null && cur.next.next != null) {
        if (cur.next.val == cur.next.next.val) {
            int x = cur.next.val;
            // 如果直接使用cur.next = cur.next.next.next; 这样没有处理到链表结尾会导致空指针
            while (cur.next != null && cur.next.val == x) {
                cur.next = cur.next.next;
            }
        } else {
            cur = cur.next;
        }
    }

    return dummy.next;
}

黄金🍄

1. 链表中环的问题

1.1 HashSet 方法

思路:

  • 遍历链表,将链表依次添加到 HashSet 中
  • 利用 HashSet 的不允许重复特性判断,如果 !(seen.add(head)) 为false,说明 HashSet 中已有同样的节点,链表为环形链表,返回 true
  • 如果不是环形链表,最后一个为null,则返回fale
/**
 * 方法1:通过HashSet判断
 *
 * @param head
 * @return
 */
public static boolean hasCycleByMap(ListNode head) {
    Set<ListNode> seen = new HashSet<ListNode>();
    while (head != null) {
        if (!seen.add(head)) {
            return true;
        }
        head = head.next;
    }
    return false;
}

1.2 快、慢指针

思路:

  1. 在链表存在环的情况下,为什么快慢指针一定会相遇?
    • 因为存在环,所以链表的最后一个肯定不为 null;快指针走2步,慢指针走1步;
  1. 如果链表没有环形,最后会退出while循环,返回false;如果有环形,则会执行slow = slow.next;

fast = fast.next.next; 直到slow == fast,返回true。

public boolean hasCycle(ListNode head) {
    if(head == null || head.next == null) {
        return false;
    }
    ListNode slow = head;
    ListNode fast = head;
    // 因为fast走的更快,所以如果没有环形的话,fast会先出现空指针情况,这里只判断fast就好了
    while(fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if(slow == fast) {
            return true;
        }
    }
    return false;
}