本文最后更新于:15 天前
破冰
漫画:什么是红黑树? - 掘金 (juejin.cn)
脑力激荡 链表基础 基础知识(青铜挑战) 单链表基础及构造方法 链表的内部结构
以下是我对单链表的理解:(2023/07/17午)
链表,是用来存储数据的一种数据结构,其由若干个节点依次连接而成。 一个节点就是一个数据元素,一个节点由两部分构成:数据域和指针域。 数据域存放数据元素的值,指针域存放指针,而该指针用来指向下一个节点。
链表的构造
链表的构造过程很简单:
创建头节点,创建head指针指向头节点
依次创建每个节点,初始化其数据域,并令前驱节点的指针域指向该节点
链表创建完成,返回该链表的head指针
下面给出具体的代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static class Node { public int val; public Node next; Node(int x) { val = x; next = null ; } @Override public String toString () { return "Node{" + "val=" + val + ", next=" + next + '}' ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private static Node initLinkedList (int [] array) { Node head = null , cur = null ; for (int i = 0 ; i < array.length; i++) { Node newNode = new Node (array[i]); if (i == 0 ) { head = newNode; cur = newNode; } else { cur.next = newNode; cur = newNode; } } return head; }
1 2 3 4 5 6 public static void main (String[] args) { int [] a = {1 , 2 , 3 , 4 , 5 , 6 }; Node head = initLinkedList(a); System.out.println(head); }
遍历链表
打印链表:头指针依次向后移动,打印每个节点的数据域
获取链表长度:头指针依次向后移动,累加节点个数,打印链表长度
代码实现如下:(2023/07/17午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static String toString (Node head) { Node current = head; StringBuilder sb = new StringBuilder (); while (current != null ) { sb.append(current.data).append("\t" ); current = current.next; } return sb.toString(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int getLength (Node head) { int length = 0 ; Node node = head; while (node != null ) { length++; node = node.next; } return length; }
链表插入
向链表中插入节点分以下三种情况:
表头插入:创建新节点,新节点指针域指向原头节点;head指针指向新节点
在表中插入:遍历到插入位置的前驱节点,依次为新节点分配后继节点和前驱节点
表尾插入:可视为 2 的特殊情况,新节点的后继节点为 NULL
代码设计如下:(2023/07/17午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static Node insertNode (Node head, Node nodeInsert, int position) { if (head == null ) { return nodeInsert; } int size = getLength(head); if (position > size + 1 || position < 1 ) { System.out.println("位置参数越界" ); return head; } if (position == 1 ) { nodeInsert.next = head; head = nodeInsert; return head; } Node pNode = head; int count = 1 ; while (count < position - 1 ) { pNode = pNode.next; count++; } nodeInsert.next = pNode.next; pNode.next = nodeInsert; return head; }
链表删除
删除链表节点同样分三种情况:
删除表头元素:head指针指向要删除节点的后继节点
删除表中元素:拿到要删除节点的前驱节点的指针域,指向要删除节点的后继节点
删除表尾元素:可视为 2 的特殊情况,要删除节点的后继节点为 NULL
代码设计如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public static Node deleteNode (Node head, int position) { if (head == null ) { return null ; } int size = getLength(head); if (position > size || position <= 0 ) { System.out.println("输入的参数有误" ); return head; } if (position == 1 ) { head = head.next; return head; } Node preNode = head; int count = 1 ; while (count < position - 1 ) { preNode = preNode.next; count++; } Node curNode = preNode.next; preNode.next = curNode.next; return head; }
双向链表设计 双向链表的内部结构
以下是我对双向链表的理解(2023/07/17午)
1 双向链表与单链表的最大区别,就是每个节点增加了一个前驱指针域,指向前驱节点
链表的构造
遍历链表
head指针依次向后移动,遍历每个节点,输出数据域的值:
链表插入
向链表中插入节点分以下三种情况:
表头插入:新建新节点,原头节点作新节点的后继节点,新节点作为原头结点的前驱节点,head指针指向新节点
表尾插入:新建新节点,原尾节点作新节点的前驱节点,新节点作为头结点的后继节点,tail指针指向新节点
链表删除
删除双向链表中的节点分以下三种情况:
表头删除:head指针指向原头节点的后继节点,并将该后继节点的前驱指针置空(2023/07/17午)
表尾删除:tail指针指向原尾节点的前驱节点,并将该前驱节点的后继指针置空
实战训练(白银挑战) 两个链表第一个公共子节点
前情提要:什么情况下,两条链表存在公共子节点呢?如下图所示:(2023/07/19午)
显而易见,c1、c2、c3均为两链表的公共子节点,且c1是两链表的第一个公共子节点
我们先废话少说,给出四种解题思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static ListNode findFirstCommonNodeByMap (ListNode pHead1, ListNode pHead2) { if (pHead1 == null || pHead2 == null ) { return null ; } ListNode current1 = pHead1; ListNode current2 = pHead2; HashMap<ListNode, Integer> hashMap = new HashMap <ListNode, Integer>(); while (current1 != null ) { hashMap.put(current1, null ); current1 = current1.next; } while (current2 != null ) { if (hashMap.containsKey(current2)) return current2; current2 = current2.next; } return null ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static ListNode findFirstCommonNodeBySet (ListNode headA, ListNode headB) { Set<ListNode> set = new HashSet <>(); while (headA != null ) { set.add(headA); headA = headA.next; } while (headB != null ) { if (set.contains(headB)) return headB; headB = headB.next; } return null ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static ListNode findFirstCommonNodeByStack (ListNode headA, ListNode headB) { Stack<ListNode> stackA = new Stack <>(); Stack<ListNode> stackB = new Stack <>(); while (headA != null ) { stackA.push(headA); headA = headA.next; } while (headB != null ) { stackB.push(headB); headB = headB.next; } ListNode preNode = null ; while (stackB.size() > 0 && stackA.size() > 0 ) { if (stackA.peek() == stackB.peek()) { preNode = stackA.pop(); stackB.pop(); } else { break ; } } return preNode; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static ListNode findFirstCommonNodeByCombine (ListNode pHead1, ListNode pHead2) { if (pHead1 == null || pHead2 == null ) { return null ; } ListNode p1 = pHead1; ListNode p2 = pHead2; while (p1 != p2) { p1 = p1.next; p2 = p2.next; if (p1 != p2) { if (p1 == null ) { p1 = pHead2; } if (p2 == null ) { p2 = pHead1; } } } return p1; }
1 2 3 if (p1 == null ) { ............... }
考虑这个问题:当前链表遍历结束后,什么情况下允许切换遍历另一条链表呢?
答案包括两种情况:未找到公共节点 / 第一次切换遍历链表结束
未找到公共节点很好理解,只有切换遍历另一条链表,才能判断是否有公共节点
第一次切换遍历链表结束,此时p1、p2指针均为null,说明两链表就没有公共节点,我们就结束链表的遍历
所以结束两链表的遍历(p1 == p2)有两种情况:第一个公共节点已找到/不存在公共节点
差和双指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static ListNode findFirstCommonNodeBySub (ListNode pHead1, ListNode pHead2) { if (pHead1 == null || pHead2 == null ) { return null ; } ListNode current1 = pHead1; ListNode current2 = pHead2; int l1 = 0 , l2 = 0 ; while (current1 != null ) { current1 = current1.next; l1++; } while (current2 != null ) { current2 = current2.next; l2++; } current1 = pHead1; current2 = pHead2; int sub = l1 > l2 ? l1 - l2 : l2 - l1; if (l1 > l2) { int a = 0 ; while (a < sub) { current1 = current1.next; a++; } } if (l1 < l2) { int a = 0 ; while (a < sub) { current2 = current2.next; a++; } } while (current2 != current1) { current2 = current2.next; current1 = current1.next; } return current1; }
上面代码里的注释,已经把解题思路解释的很清晰了
基于我个人的理解,下面讲解一下这些方法的共同点,也就是解题思路的形成过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 我们的目标是:查出两条链表的第一个公共节点 公共节点是什么我们已经搞清楚了,那如何拿到第一个公共节点呢? 不论是分别正序/倒序遍历两条链表,我们的执行思路始终是: 从两链表的头节点/尾节点开始,分别依次向后遍历链表的每个节点,再比较两节点,判断它们是否相同,即是否为两链表的公共节点 我们能够判断出两链表的公共节点,那么第一个公共节点就好找了: 如果遍历顺序为正序,则选出第一组公共节点;如果遍历顺序为倒序,则选出最后一组公共节点 只需要根据正序/倒序遍历链表,选出第一组公共节点/ 最后一组公共节点,就找到了两链表的第一个公共节点 这里问题来了,我们要明确一点,即两链表的长度不一定相同 这就带来了问题: 我们上面查找两链表公共节点的思路,其实只有在两链表长度相同时,才行得通 那我们的目标就是,如何构造出两链表长度相同的环境: 哈希和集合:直接消除了链表长度带来的影响,通过开辟了新的空间,判断节点是否相等,进而查找出两链表的公共节点 栈、两链表拼接、差和双指针:本质上都是构造出两链表长度相同的环境,进而查找出两链表的公共节点
这就是查找两链表的第一个公共节点的解题思路了,希望对你有帮助
回文链表的判断
给出一个链表,判断其是否为回文链表,那什么是回文链表?
以下即为一条回文链表:
即对回文链表正序遍历和倒序遍历,得到的结果是一样的
这种题解法很多,我们列举常见的、简单的且容易理解的解法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static boolean isPalindromeByAllStack (ListNode head) { ListNode temp = head; Stack<Integer> stack = new Stack <>(); while (temp != null ) { stack.push(temp.val); temp = temp.next; } while (head != null ) { if (head.val != stack.pop()) { return false ; } head = head.next; } return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static boolean isPalindromeByHalfStack (ListNode head) { if (head == null ) return true ; ListNode temp = head; Stack<Integer> stack = new Stack <>(); int len = 0 ; while (temp != null ) { stack.push(temp.val); temp = temp.next; len++; } len >>= 1 ; while (len-- >= 0 ) { if (head.val != stack.pop()) return false ; head = head.next; } return true ; }
倒序链表法,代码如下:
根据原链表构造一条倒序链表,遍历这两条链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static boolean isPalindromeByReverseList (ListNode head) { ListNode newHead = head, temp = head; while (temp != null ) { ListNode node = new ListNode (temp.val); node.next = newHead; newHead = node; temp = temp.next; } while (newHead != null && head != null ) { if (head.val != newHead.val) return false ; head = head.next; newHead = newHead.next; } return true ; }
此外还有双指针法(之后双指针专题练习结束后回来补充)、递归法(不推荐掌握,容易绕晕)
合并两条有序链表
常见的解法就是构造第三条链表,然后依次遍历两条有序链表,比较各节点大小,依次连接到新链表中,整个过程如下图所示:
由于两条链表长度不一定相同,可能出现一条链表遍历完,另一条链表还没有的情况,这其实是一个优化点
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static ListNode mergeTwoLists (ListNode list1, ListNode list2) { ListNode newHead = new ListNode (-1 ); ListNode res = newHead; while (list1 != null || list2 != null ) { if (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 = list2; list2 = list2.next; newHead = newHead.next; newHead.next = list1; list1 = list1.next; } newHead = newHead.next; } else if (list1 != null && list2 == null ) { newHead.next = list1; list1 = list1.next; newHead = newHead.next; } else if (list1 == null && list2 != null ) { newHead.next = list2; list2 = list2.next; newHead = newHead.next; } } return res.next; }
上面的解法当中,我们把两条链表是否都为空/只有一条为空放在了一个循环下,这次我们把它拆开来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public static ListNode mergeTwoLists2 (ListNode list1, ListNode list2) { ListNode newHead = new ListNode (-1 ); ListNode res = newHead; while (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 = list2; list2 = list2.next; newHead = newHead.next; newHead.next = list1; list1 = list1.next; } newHead = newHead.next; } while (list1 != null ) { newHead.next = list1; list1 = list1.next; newHead = newHead.next; } while (list2 != null ) { newHead.next = list2; list2 = list2.next; newHead = newHead.next; } return res.next; }
一条链表合合并完成后,直接拼接剩余的另一条链表即可(2023/10/09晚)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static ListNode mergeTwoListsMoreSimple (ListNode l1, ListNode l2) { ListNode prehead = new ListNode (-1 ); ListNode prev = prehead; while (l1 != null && l2 != null ) { if (l1.val <= l2.val) { prev.next = l1; l1 = l1.next; } else { prev.next = l2; l2 = l2.next; } prev = prev.next; } prev.next = l1 == null ? l2 : l1; return prehead.next; }
合并K个链表
每次两条链表合成完毕后,返回新链表的头节点,再用这条列表,继续与剩余列表合成 (2023/10/09晚)
1 2 3 4 5 6 7 8 9 10 11 12 13 public static ListNode mergeKLists (ListNode[] lists) { ListNode res = null ; for (ListNode list : lists) { res = mergeTwoListsMoreSimple(res, list); } return res; }
简单的合并链表
随便给你两条链表,你会怎么连接这两条链表?(比如:将链表b连接到链表a后面)
正确的思路只有一个,那就是拿到链表a的尾节点,拿到链表b的头节点,作:a.next = b,连接完成
举个例子:将链表a的[a,b]区间删掉,把链表b连接进去,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static ListNode mergeInBetween (ListNode listA, int a, int b, ListNode listB) { ListNode preA = listA; ListNode postA = listA; ListNode postB = listB; int i = 0 , j = 0 ; while (postA != null && preA != null && j < b) { if (i < a - 1 ) { preA = preA.next; i++; } if (j != b) { postA = postA.next; j++; } } while (postB.next != null ) { postB = postB.next; } preA.next = listB; postB.next = postA; return preA; }
双指针
寻找中间节点
快慢指针均指向头节点
快指针一次跳俩步,慢指针一次跳一步,两指针同时移动
当快指针指向节点为空(偶数个节点)或快指针指向节点的后继节点为空(奇数个节点)时,两指针停止移动
此时,慢指针指向链表中间节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; }
寻找倒数第K个节点
快慢指针均指向头节点
快指针跳到第K+1个节点,此时慢指针与快指针相距K个节点
快慢指针同时移动,当快指针指向链表末端(即空节点)时,两指针停止移动
此时,慢指针指向链表的倒数第K个节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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; }
寻找倒数第 K 个节点还有两种方法:遍历链表法和压栈法
遍历链表:先遍历一遍链表,得到链表长度 L,再遍历一遍链表,取第 L-K+1个节点
压栈:将链表压入栈,再出栈,取第 K 个出栈的节点
这两种方法很好理解,具体代码择日实现(2023/07/24晚)
旋转链表
常见的情景题:把链表的每个节点,都向右移动K个位置
这个是有两种思路的:反转链表、转化为寻找倒数第 K-1 个节点
反转链表暂且不表,这里可以看看第二种方法:转化为寻找倒数第 K-1 个节点
把链表的每个节点,都向右移动K个位置 => 把链表的后 K 个节点,都旋转成前 K 个节点
那就把问题转换成了:转化为寻找倒数第 K-1 个节点:
此时慢指针指向了倒数第 K-1 个节点,快指针指向了链表的尾节点
倒数第 K 个节点为头节点(断掉慢指针指向节点的后继,快指针指向原头节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 ; while (head != null ) { head = head.next; len++; } if (k % len == 0 ) { return temp; } while ((k % len) > 0 ) { k--; fast = fast.next; } while (fast.next != null ) { fast = fast.next; slow = slow.next; } ListNode res = slow.next; slow.next = null ; fast.next = temp; return res; }
删除特定节点
这类型题目本身不难,因为我们之前学过删除节点,但删除节点有两种情况:删除头节点和删除尾节点
这两种情况的处理方式是不一样的,所以我们提供一个全新的思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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; }
删除倒数第K个节点
我们之前学过如何查找倒数第K个节点,它们本质上是一样的,我们还是提供三种思路:
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static ListNode removeNthFromEndByLength (ListNode head, int n) { ListNode dummy = new ListNode (0 ); dummy.next = head; int length = getLength(head); ListNode cur = dummy; for (int i = 1 ; i < length - n + 1 ; ++i) { cur = cur.next; } cur.next = cur.next.next; return dummy.next; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static ListNode removeNthFromEndByStack (ListNode head, int n) { ListNode dummy = new ListNode (0 ); dummy.next = head; Deque<ListNode> stack = new LinkedList <ListNode>(); ListNode cur = dummy; while (cur != null ) { stack.push(cur); cur = cur.next; } for (int i = 0 ; i < n; ++i) { stack.pop(); } ListNode prev = stack.peek(); assert prev != null ; prev.next = prev.next.next; return dummy.next; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static ListNode removeNthFromEndByTwoPoints (ListNode head, int n) { ListNode dummy = new ListNode (0 ); dummy.next = head; ListNode first = head; ListNode second = dummy; for (int i = 0 ; i < n; ++i) { first = first.next; } while (first != null ) { first = first.next; second = second.next; } assert second.next != null ; second.next = second.next.next; return dummy.next; }
删除重复节点
删除重复节点当然有两种情况了:仅留一个或者删除全部,废话少说,直接上代码:
重复元素保留一个,在这种情况下,被删除的节点肯定不可能是头节点 (头节点删除时的处理是不一样的)2023/11/03晚
所以此处不用考虑删除节点是头节点还是中间节点了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static ListNode deleteDuplicates (ListNode head) { if (head == null ) { return null ; } 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; while (cur.next != null && cur.next.val == x) { cur.next = cur.next.next; } } else { cur = cur.next; } } return dummy.next; }
通关(过关挑战)
题目内容:
算法训练营开课了,小伙伴们踊跃报名,请用链表来帮忙统计学员信息:
学院方向不同:Java、Python、C++,仅有一条链表,其前中后三部分,分别是:Java、Python、C++的同学
每种语言都会不断有学生进来,每次都要将对应的同学插入到对应的段的末尾
具体代码如下:(2023/07/27早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 public class InsertStudent { public static void main (String[] args) { ListNode node1 = new ListNode ("Node 1" , "Java" ); ListNode node2 = new ListNode ("Node 2" , "Python" ); ListNode node3 = new ListNode ("Node 3" , "C++" ); ListNode[] nodes = new ListNode [3 ]; nodes[0 ] = node1; nodes[1 ] = node2; nodes[2 ] = node3; ListNode head = initLinkList(nodes); ListNode node4 = new ListNode ("Node 4" , "Java" ); ListNode node5 = new ListNode ("Node 5" , "C++" ); ListNode node6 = new ListNode ("Node 6" , "Python" ); ListNode node8 = new ListNode ("Node 8" , "C++" ); ListNode node9 = new ListNode ("Node 9" , "Python" ); ListNode node7 = new ListNode ("Node 7" , "Java" ); insertStudentByLanguage(node4, head); insertStudentByLanguage(node5, head); insertStudentByLanguage(node6, head); insertStudentByLanguage(node7, head); insertStudentByLanguage(node8, head); insertStudentByLanguage(node9, head); printLinkList(head); } public static void insertStudentByLanguage (ListNode node, ListNode head) { ListNode cur = head; String language = node.language; switch (language) { case "Java" : while (!cur.next.language.equals("Python" )) { cur = cur.next; } node.next = cur.next; cur.next = node; break ; case "Python" : while (!cur.next.language.equals("C++" )) { cur = cur.next; } node.next = cur.next; cur.next = node; break ; case "C++" : while (cur.next != null ) { cur = cur.next; } cur.next = node; break ; default : break ; } } public static void printLinkList (ListNode head) { ListNode temp = head; while (temp != null ) { System.out.println(temp + "--> " ); temp = temp.next; } } public static ListNode initLinkList (ListNode[] array) { int i = 0 ; ListNode head = null , cur = null ; while (i < array.length) { ListNode newNode = new ListNode (array[i].name, array[i].language); if (head == null ) { head = newNode; cur = head; } else { cur.next = newNode; cur = newNode; } i++; } return head; } static class ListNode { public String name; public String language; public ListNode next; public ListNode (String name, String language) { this .name = name; this .language = language; } @Override public String toString () { return "ListNode{" + "name='" + name + '\'' + ", language='" + language + '\'' + '}' ; } } }
反转链表 基础知识 + 基本操作(青铜挑战)
常见的链表反转的方法:虚拟头节点、直接反转、递归
虚拟头节点,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static ListNode reverseListByDummyNotCreate (ListNode head) { ListNode ans = new ListNode (-1 ); ListNode cur = head; while (cur != null ) { ListNode next = cur.next; cur.next = ans.next; ans.next = cur; cur = next; } return ans.next; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static ListNode reverseListSimple (ListNode head) { ListNode prev = null ; ListNode curr = head; while (curr != null ) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; }
拓展训练(白银挑战) 区间反转链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static ListNode reverseBetween2 (ListNode head, int left, int right) { ListNode dummyNode = new ListNode (-1 ); dummyNode.next = head; ListNode pre = dummyNode; for (int i = 0 ; i < left - 1 ; i++) { pre = pre.next; } ListNode cur = pre.next; ListNode next; for (int i = 0 ; i < right - left; i++) { next = cur.next; cur.next = next.next; next.next = pre.next; pre.next = next; } return dummyNode.next; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static ListNode reverseBetween (ListNode head, int left, int right) { ListNode dummyNode = new ListNode (-1 ); dummyNode.next = head; ListNode pre = dummyNode; for (int i = 0 ; i < left - 1 ; i++) { pre = pre.next; } ListNode rightNode = pre; for (int i = 0 ; i < right - left + 1 ; i++) { rightNode = rightNode.next; } ListNode leftNode = pre.next; ListNode succ = rightNode.next; pre.next = null ; rightNode.next = null ; reverseList(leftNode); pre.next = rightNode; leftNode.next = succ; return dummyNode.next; }
两两交换链表中的节点
这就是区间反转链表的拓展了,无非是区间长度固定为2,一次性反转多个区间
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static ListNode swapPairs (ListNode head) { ListNode dummyHead = new ListNode (0 ); dummyHead.next = head; ListNode cur = dummyHead; while (cur.next != null && cur.next.next != null ) { ListNode node1 = cur.next; ListNode node2 = cur.next.next; cur.next = node2; node1.next = node2.next; node2.next = node1; cur = node1; } return dummyHead.next; }
链表相加
1 2 3 4 输入:1 -> 3 -> 6 输出:4 -> 1 -> 3 2 -> 7 -> 7 即:136 + 277 = 413
即将链表的数据域相连,当作数字进行相加操作,从低位开始相加,涉及到进位,有两种实现思路:
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public static ListNode addInListByStack (ListNode head1, ListNode head2) { Stack<ListNode> st1 = new Stack <ListNode>(); Stack<ListNode> st2 = new Stack <ListNode>(); while (head1 != null ) { st1.push(head1); head1 = head1.next; } while (head2 != null ) { st2.push(head2); head2 = head2.next; } ListNode dummy = new ListNode (-1 ); int carry = 0 ; while (!st1.empty() || !st2.empty() || carry != 0 ) { ListNode a = new ListNode (0 ); ListNode b = new ListNode (0 ); if (!st1.empty()) { a = st1.pop(); } if (!st2.empty()) { b = st2.pop(); } int get_sum = a.val + b.val + carry; int ans = get_sum % 10 ; carry = get_sum / 10 ; ListNode cur = new ListNode (ans); cur.next = dummy.next; dummy.next = cur; } return dummy.next; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static ListNode addInListByReverse (ListNode head1, ListNode head2) { head1 = reverse(head1); head2 = reverse(head2); ListNode head = new ListNode (-1 ); ListNode cur = head; int carry = 0 ; while (head1 != null || head2 != null ) { int val = carry; if (head1 != null ) { val += head1.val; head1 = head1.next; } if (head2 != null ) { val += head2.val; head2 = head2.next; } cur.next = new ListNode (val % 10 ); carry = val / 10 ; cur = cur.next; } if (carry > 0 ) { cur.next = new ListNode (carry); } return reverse(head.next); }
奇怪的力扣试题(链表相加)
妈的,今晚再提交一道题就完成任务了,我还选择了这道题,原以为会很顺利(2023/11/10晚)
🍖 力扣原题链接:2. 两数相加 - 力扣(LeetCode)
逼着我认真分析了一遍构造链表的几种方法,捋了捋链表节点插入的各种场景(直接插入 / 虚拟头节点 ,前插 / 后插 )
没有解决问题,最后发现题目要求是这样的:
妈的,什么破题,今晚不搞这道题了 🍟(2023/11/10晚)
单链表加一
1 2 输入:1 -> 3 -> 6 输出:1 -> 3 -> 7
本质上就是链表相加,只不过另外一条链表只有一个节点,从低位开始加一,涉及到进位,同样有两种实现思路:
这里仅展示压栈法,具体代码如下:(2023/07/31晚)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static ListNode plusOne (ListNode head) { Stack<Integer> st = new Stack (); while (head != null ) { st.push(head.val); head = head.next; } int carry = 0 ; int adder = 1 ; ListNode dummy = new ListNode (0 ); while (!st.empty() || adder != 0 || carry > 0 ) { int digit = st.pop(); int sum = digit + adder + carry; carry = sum >= 10 ? 1 : 0 ; sum = sum >= 10 ? sum - 10 : sum; ListNode cur = new ListNode (sum); cur.next = dummy.next; dummy.next = cur; adder = 0 ; } return dummy.next; }
通关(过关挑战)
数组 基础知识(青铜挑战)
有关线性表的内容,这里不过多介绍,详情可看讲义(2023/08/09早)
数组的基本操作:
初始化数组:
1 2 3 4 5 6 7 8 public static void initArray () { int [] arr = new int [10 ]; int [] arr2 = new int []{1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; int [] arr3 = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int findByElement (int [] arr, int size, int element) { for (int i = 0 ; i < size; i++) { if (arr[i] == element) return element; } return -1 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static int addByElementSequence (int [] arr, int size, int element) { if (size >= arr.length) return -1 ; int index = size; for (int i = 0 ; i < size; i++) { if (element < arr[i]) { index = i; break ; } } for (int j = size; j > index; j--) { arr[j] = arr[j - 1 ]; } arr[index] = element; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static int removeByElement (int [] arr, int size, int key) { int index = -1 ; for (int i = 0 ; i < size; i++) { if (arr[i] == key) { index = i; break ; } } if (index != -1 ) { for (int i = index + 1 ; i < size; i++) { arr[i - 1 ] = arr[i]; } size--; } return size; }
单调数组问题
数组单调有两种情况:单调递增、单调递减
如何判断数组是否为单调数组呢?(2023/08/11早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static boolean isMonotonic (int [] nums) { return isSorted(nums, true ) || isSorted(nums, false ); } public static boolean isSorted (int [] nums, boolean increasing) { int n = nums.length; for (int i = 0 ; i < n - 1 ; ++i) { if (increasing) { if (nums[i] > nums[i + 1 ]) { return false ; } } else { if (nums[i] < nums[i + 1 ]) { return false ; } } } return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static boolean isMonotonic_2 (int [] nums) { boolean inc = true , dec = true ; int n = nums.length; for (int i = 0 ; i < n - 1 ; ++i) { if (nums[i] > nums[i + 1 ]) { inc = false ; } if (nums[i] < nums[i + 1 ]) { dec = false ; } } return inc || dec; }
简单的二分查找
在单调数组中查找目标元素,最便捷的方法就是二分查找:(2023/08/11早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static int search (int [] nums, int target) { int n = nums.length; if (n == 0 ) { return -1 ; } int left = 0 , right = n - 1 ; while (left <= right) { int mid = left + (right - left) / 2 ; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid + 1 ; } else { right = mid - 1 ; } } return -1 ; }
数组合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void merge1 (int [] nums1, int nums1_len, int [] nums2, int nums2_len) { for (int i = 0 ; i < nums2_len; ++i) { nums1[nums1_len + i] = nums2[i]; } Arrays.sort(nums1); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void merge2 (int [] nums1, int nums1_len, int [] nums2, int nums2_len) { int i = nums1_len + nums2_len - 1 ; int len1 = nums1_len - 1 , len2 = nums2_len - 1 ; while (len1 >= 0 && len2 >= 0 ) { if (nums1[len1] <= nums2[len2]) nums1[i--] = nums2[len2--]; else if (nums1[len1] > nums2[len2]) nums1[i--] = nums1[len1--]; } while (len2 != -1 ) nums1[i--] = nums2[len2--]; while (len1 != -1 ) nums1[i--] = nums1[len1--]; }
实战训练(白银挑战) 双指针
我们使用双指针,能够很清晰、方便地解决问题,接下来我们使用双指针来解决一些典型问题:
删除重复元素
给定一个数组,要求删除其中所有重复元素,重复元素保留一个
对于这种问题,我们使用快慢双指针来解决:
具体代码实现如下:(2023/08/13午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static int removeDuplicates (int [] nums) { int slow = 0 ; for (int fast = 1 ; fast < nums.length; fast++) { if (nums[fast] != nums[slow]){ slow++; nums[slow] = nums[fast]; } } return slow + 1 ; }
删除指定元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static int removeElement (int [] nums, int val) { int slow = 0 ; for (int fast = 0 ; fast < nums.length; fast++) { if (nums[fast] != val) { nums[slow] = nums[fast]; slow++; } } return slow; }
这里简单捋一捋对撞双指针的思路:(2023/08/13午)
左指针在左,右指针在右,左指针向前移动
如果左指针未找到指定元素,则左指针继续向前移动
如果左指针找到了指定元素,就拿右指针所指元素覆盖,左指针向前移动,右指针向后移动
循环执行,直到两指针碰撞
即:左指针左侧维护了所有正确的元素值
使用对撞双指针解决思路:(2023/08/15晚)
设左右双指针
左指针向前移动,遇到指定元素就停下来
这时,将两指针所指元素交换,右指针向前一步
重复以上操作,直至两指针相撞
具体代码实现如下:(2023/08/13午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static int removeElement3 (int [] nums, int val) { int right = nums.length - 1 ; for (int left = 0 ; left < right; ) { if (nums[left] == val) { nums[left] = nums[right]; right--; } left++; } return right; }
元素奇偶移动专题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static int [] sortArrayByParity(int [] A) { int left = 0 , right = A.length - 1 ; while (left < right) { if (A[left] % 2 > A[right] % 2 ) { int tmp = A[left]; A[left] = A[right]; A[right] = tmp; } if (A[left] % 2 == 0 ) left++; if (A[right] % 2 == 1 ) right--; } return A; }
数组轮转
情景:
给你一个数组,将数组的每个元素,向右轮转k个位置,得到新数组,即:
1 2 输入: nums = [1,2,3,4,5,6,7] , k = 3 输出: [5,6,7,1,2,3,4]
看起来好像挺简单,但怎么实现没思路,这里介绍一种简单的方法:两轮反转(2023/08/25早)
首先,对整个数组实行翻转
然后,从k处将数组分隔,分别翻转分隔后的两个数组
这样就得到了最终结果
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void rotate (int [] nums, int k) { k %= nums.length; reverse(nums, 0 , nums.length - 1 ); reverse(nums, 0 , k - 1 ); reverse(nums, k, nums.length - 1 ); } public static void reverse (int [] nums, int start, int end) { while (start < end) { int temp = nums[start]; nums[start] = nums[end]; nums[end] = temp; start += 1 ; end -= 1 ; } }
数组的区间专题
1 2 3 示例: 输入: nums = [0 ,1 ,2 ,4 ,5 ,7 ] 输出: ["0->2" ,"4->5" ,"7" ]
实现思路:
定义快慢指针
快指针向前移动
快指针找到了不连续递增的数,慢指针指向该数,快指针继续移动
快指针到达数组边缘,结束
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static List<String> summaryRanges (int [] nums) { List<String> res = new ArrayList <>(); int slow = 0 ; for (int fast = 0 ; fast < nums.length; fast++) { if (fast + 1 == nums.length || nums[fast] + 1 != nums[fast + 1 ]) { StringBuilder sb = new StringBuilder (); sb.append(nums[slow]); if (slow != fast) { sb.append("->" ).append(nums[fast]); } res.add(sb.toString()); slow = fast + 1 ; } } return res; }
字符串替换空格问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static String replaceSpace1 (StringBuffer str) { String res = "" ; for (int i = 0 ; i < str.length(); i++) { char c = str.charAt(i); if (c == ' ' ) res += "%20" ; else res += c; } return res; }
我们创建了一个新的字符串来完成👆,那么如果我们在原字符串上,扩充所需空间,不必开辟新的空间就能完成呢?
思路如下:
扩充字符串长度
定义快慢指针,快指针指向原长度末,慢指针指向现长度末
快指针向前移动,检查每个字符
如果该字符是为非空格字符,则慢指针维护该字符,两指针向前移动
如果是空格,慢指针维护”20%”,两指针向前移动
快指针到达数组边缘,结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static String replaceSpace2 (StringBuffer str) { if (str == null ) return null ; int numOfblank = 0 ; int len = str.length(); for (int i = 0 ; i < len; i++) { if (str.charAt(i) == ' ' ) numOfblank++; } str.setLength(len + 2 * numOfblank); int fast = len - 1 ; int slow = (len + 2 * numOfblank) - 1 ; while (fast >= 0 && slow > fast) { char c = str.charAt(fast); if (c == ' ' ) { fast--; str.setCharAt(slow--, '0' ); str.setCharAt(slow--, '2' ); str.setCharAt(slow--, '%' ); } else { str.setCharAt(slow, c); fast--; slow--; } } return str.toString(); }
栈 基础知识(青铜挑战)
认识栈这个数据结构:先进先出
常用的栈操作有:
push(E e):入栈
peek():弹出栈顶元素
pop():出栈
empty():判断栈是否为空
我们尝试手动创建一个栈数据结构,并实现它的基础功能:
基于数组实现栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 class Mystack2 <T> { private Object[] stack; private int top; public Mystack2 (int size) { stack = new Object [size]; } public void push (T t) { this .expandCapacity(top + 1 ); this .stack[top] = t; top++; } public T peek () { T t = null ; if (!this .empty()) t = (T) this .stack[top - 1 ]; return t; } public T pop () { T t = this .peek(); this .stack[top - 1 ] = null ; top--; return t; } public boolean empty () { return this .top <= 0 ; } public void expandCapacity (int size) { int capacity = this .stack.length; if (size > capacity) { capacity = capacity * 3 / 2 + 1 ; this .stack = Arrays.copyOf(stack, capacity); } } public static void main (String[] args) { Mystack2<String> stack = new Mystack2 <>(10 ); System.out.println(stack.peek()); System.out.println(stack.empty()); stack.push("java" ); stack.push("is" ); stack.push("beautiful" ); stack.push("language" ); System.out.println(stack.pop()); System.out.println(stack.empty()); System.out.println(stack.peek()); } }
基于链表实现栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class ListStack2 <T> { private Node head; public void push (T t) { if (t == null ) { throw new NullPointerException ("参数不能为空" ); } if (head == null ) { head = new Node (t); } else { Node node = new Node (t); node.next = head; head = newNode; } } public T peek () { return head.t; } public T pop () { head = head.next; return head.t; } public boolean empty () { return head == null ; } class Node { private T t; private Node next; public Node (T t) { this .t = t; } } }
实战训练(白银挑战) 括号匹配问题
情景:
给定一个字符串,仅包含”(“、”)”、”[“、”]”等字符,要求如下:
如果该字符中的左括号与右括号连续且成对出现,则说明该字符串有效,如下:
实现一个算法,判断该字符串是否有效
实现思路:
构造一个HashMap,将左右括号以键值对形式存放进去
构造一个栈Stack
依次遍历字符串中的字符s,如果存在Map中的key值与s相等,则入栈
如果该字符s不是key值,即不是左括号,则Stack出栈,计算value值是否与s相等
不相等则说明括号不连续或不成对 ,该字符串是无效的
这里利用的是栈的特性:先进先出,用来判断括号是否连续且成对(2023/08/26午)
具体代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 static boolean isValid (String s) { if (s.length() <= 1 ) { return false ; } Map<Character, Character> smap = new HashMap <>(); smap.put('(' , ')' ); smap.put('{' , '}' ); smap.put('[' , ']' ); Stack<Character> stack = new Stack <>(); for (int i = 0 ; i < s.length(); i++) { char item = s.charAt(i); if (smap.containsKey(item)) { stack.push(item); } else { if (stack.isEmpty()) { return false ; } Character left = stack.pop(); char rightChar = smap.get(left); if (rightChar != item) { return false ; } } } return stack.isEmpty(); }
最小栈
实现快速获取栈中的最小元素
我们设计这样一个数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class MinStack { private Stack<Integer> stack; private Stack<Integer> minStack; public MinStack () { stack = new Stack <>(); minStack = new Stack <>(); } public void push (int x) { stack.push(x); if (minStack.isEmpty() || x <= minStack.peek()) { minStack.push(x); } } public void pop () { if (stack.peek().equals(minStack.peek())) { minStack.pop(); } stack.pop(); } public int top () { return stack.peek(); } public int getMin () { return minStack.peek(); } }
最大栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class MaxStack { private Stack<Integer> stack; private Stack<Integer> maxStack; public MaxStack () { stack = new Stack <>(); maxStack = new Stack <>(); } public void push (int x) { stack.push(x); if (maxStack.isEmpty() || x >= maxStack.peek()) { maxStack.push(x); } } public void pop () { if (stack.peek().equals(maxStack.peek())) { maxStack.pop(); } stack.pop(); } public int top () { return stack.peek(); } public int getMax () { return maxStack.peek(); } }
解释:
对于实现最大栈的pop()方法:
当你从主栈中弹出一个元素,你会检查这个元素是否是辅助栈的栈顶元素 。如果是,那么辅助栈的栈顶元素会被弹出。无论这个元素是否是辅助栈的栈顶元素,主栈的元素都会被弹出。
这样做的目的是为了维护辅助栈的栈顶元素始终是主栈当前的最大元素 。当主栈的元素被弹出时,如果它不是最大的元素(即它不是辅助栈的栈顶元素),那么辅助栈的栈顶元素(即当前的最大元素)就不会被弹出。
因此,通过这种方式,辅助栈可以持续地维护主栈中的最大值。
实在理不清过程的,强烈建议画个图 ,很直观 ,一目了然 ,尤其是栈、队列这种数据结构(2023/11/10晚)
通关(过关挑战)
1 给你一个栈,依次入栈1 2 3 4 ,请问以下出栈顺序可能出现吗?为什么?
1 2 出栈顺序:2 3 4 1 √ 先出栈2,必然是1已经入栈了,再入栈3、出栈3,入栈4、出栈4,最后出栈1,没有问题
1 2 出栈顺序:1 4 2 3 × 先入栈1、出栈1,要出栈4,必然是2 3 4依次入栈了,出栈4之后的出栈顺序只能是3 2,而不可能是2 3,故错误
多做这样的例子,你就能彻底明白栈这个数据结构了:先进先出
队列和Hash 基础知识(青铜挑战) Hash
Hash,又称散列,将任意长度的输入,通过散列算法,转换为固定长度的输出
映射、扩容机制
碰撞处理(Hash冲突)
开放寻址法
链地址法
再哈希法(布隆过滤)
建立公共溢出区
队列
特点:先进先出(2023/09/03午)
用链表实现队列
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class LinkQueue { private Node front; private Node rear; private int size; public LinkQueue () { this .front = new Node (0 ); this .rear = new Node (0 ); } public void push (int value) { Node newNode = new Node (value); Node temp = front; while (temp.next != null ) { temp = temp.next; } temp.next = newNode; rear = newNode; size++; } public int pull () { if (front.next == null ) { System.out.println("队列已空" ); } Node firstNode = front.next; front.next = firstNode.next; size--; return firstNode.data; } public void traverse () { Node temp = front.next; while (temp != null ) { System.out.print(temp.data + "\t" ); temp = temp.next; } } static class Node { public int data; public Node next; public Node (int data) { this .data = data; } } public static void main (String[] args) { LinkQueue linkQueue = new LinkQueue (); linkQueue.push(1 ); linkQueue.push(2 ); linkQueue.push(3 ); System.out.println("第一个出队的元素为:" + linkQueue.pull()); System.out.println("队列中的元素为:" ); linkQueue.traverse(); } }
实战训练(白银挑战) 用栈实现队列
情景:使用栈实现队列
实现思路:(2023/09/03午)
利用栈先进后出的特性,首先一个栈inStack将元素压栈,它负责 “进” 元素
inStack再依次出栈,另一个栈outStack负责将出栈的元素压栈,它负责 “出” 元素
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class MyQueue { Deque<Integer> inStack; Deque<Integer> outStack; public MyQueue () { inStack = new LinkedList <Integer>(); outStack = new LinkedList <Integer>(); } public void push (int x) { inStack.push(x); } public int pop () { if (outStack.isEmpty()) { in2out(); } return outStack.pop(); } public int peek () { if (outStack.isEmpty()) { in2out(); } return outStack.peek(); } public boolean empty () { return inStack.isEmpty() && outStack.isEmpty(); } private void in2out () { while (!inStack.isEmpty()) { outStack.push(inStack.pop()); } } }
用队列实现栈
初始化阶段 :创建两个队列,queue1
和queue2
。这两个队列开始时都是空的。topElement
变量用来跟踪当前栈顶的元素。
push操作 :当执行push操作时,将新元素添加到queue1
的尾部。同时,因为这个新元素就是当前的栈顶元素,所以更新topElement
为这个新元素。
pop操作 :当执行pop操作时,首先将queue1
中的所有元素(除了最后一个)依次弹出并添加到queue2
中。这样做的效果是,queue2
中的元素就是queue1
中元素的逆序,即最后一个进入的元素现在成为了第一个。然后,将queue1
和queue2
交换,使得queue2
成为了主要的队列。最后,从queue2
中弹出的最后一个元素就是原来的栈顶元素,返回这个元素。
peek操作 :这个操作只需要返回当前的topElement
即可,不需要进行任何操作。
isEmpty操作 :检查queue1
是否为空,如果为空,那么栈就是空的,返回true;否则返回false。
核心就是:两队列交换,queue1始终负责入栈,queue始终负责出栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class StackUsingQueues { private Queue<Integer> queue1; private Queue<Integer> queue2; private int topElement; public StackUsingQueues () { queue1 = new LinkedList <>(); queue2 = new LinkedList <>(); } public void push (int x) { queue1.add(x); topElement = x; } public int pop () { while (queue1.size() > 1 ) { queue2.add(queue1.remove()); } int poppedElement = queue1.remove(); Queue<Integer> temp = queue1; queue1 = queue2; queue2 = temp; return poppedElement; } public int peek () { return topElement; } public boolean isEmpty () { return queue1.isEmpty(); } }
还是看不明白的,强烈建议画个图 ,一目了然,就算忘了也能快速回忆起来:(2023/11/10晚)
两数之和
1 2 3 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:2 + 7 = 9, 返回二者下标
解决思路有三种,这里统一讲:
遍历数组,依次两两相加 ,判断是否等于 target。缺点:双层循环嵌套 —> 时间复杂度大
建立一张Hash表,存储 target - x,存在即返回,不存在即存储 x,继续遍历 —> 遍历过的数字已经存储到map中了,减少了遍历元素的个数
排序数组,将二层循环改为二分查找
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 public static int [] twoSum(int [] nums, int target) { int n = nums.length; for (int i = 0 ; i < n; ++i) { for (int j = i + 1 ; j < n; ++j) { if (nums[i] + nums[j] == target) { return new int []{i, j}; } } } return new int [0 ]; }
1 2 3 4 5 6 7 8 9 10 11 public int [] twoSum3(int [] nums, int target) { HashMap<Integer, Integer> map = new HashMap <>(); for (int i = 0 ; i < nums.length; i++) { if (map.containsKey(target - nums[i])) { return new int []{i, map.get(target - nums[i])}; } map.put(nums[i], i); } return new int [0 ]; }
三数之和 二叉树遍历(层数优先) 基础知识(青铜挑战)
实战训练(白银挑战) 简单的层序遍历
基本的层序遍历思路很清晰:
给你一个二叉树根节点,你需要创建一个队列 queue 来遍历节点,一个链表 list 来存储节点的数据域,即值
首先将根节点入队
队列 queue 出队,将该节点值存入 list,再依次将左右孩子节点入队
重复以上操作,每个节点出对后,都存储该节点值到 list 中,再依次将左右孩子节点入队,直到队列 queue为空
这样得到的数据链表 list 就是按层序遍历的顺序得到的
具体代码如下:(2023/09/09午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static List<Integer> simpleLevelOrder (TreeNode root) { if (root == null ) { return new ArrayList <Integer>(); } List<Integer> res = new ArrayList <Integer>(); LinkedList<TreeNode> queue = new LinkedList <TreeNode>(); queue.add(root); while (queue.size() > 0 ) { TreeNode t = queue.remove(); res.add(t.val); if (t.left != null ) { queue.add(t.left); } if (t.right != null ) { queue.add(t.right); } } return res; }
层序遍历分层
层序遍历我们做到了,这里添加一个要求:对层序遍历的节点值分层处理,即二叉树每层的节点值分别存放进一个链表 list 中
这个代码怎么写呢?很简单的,按这个思路走:
我们之前层序遍历时,每出队一个节点,都把其值存入 list 链表中,然后入队其孩子节点
在开始出队某一层的节点时,此时队列的节点数,就是二叉树这一层的节点数
那我们根据可以某时刻队列容量来遍历队列,将这层的节点全部出队,并且把节点值存入该层独有的 list 中
当然了,每个节点出队后,要将自己的孩子节点依次入队
这样,当队列为空时,我们得到了各层的节点链表 list,返回一个包含各层 list 的 list 即可
具体代码如下:(2023/09/09晚)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static List<List<Integer>> level102Order (TreeNode root) { if (root == null ) { return new ArrayList <List<Integer>>(); } List<List<Integer>> res = new ArrayList <List<Integer>>(); LinkedList<TreeNode> queue = new LinkedList <TreeNode>(); queue.add(root); while (queue.size() > 0 ) { int size = queue.size(); ArrayList<Integer> tmp = new ArrayList <Integer>(); for (int i = 0 ; i < size; ++i) { TreeNode t = queue.remove(); tmp.add(t.val); if (t.left != null ) { queue.add(t.left); } if (t.right != null ) { queue.add(t.right); } } res.add(tmp); } return res; }
自底向上的层序遍历
在前面学习的基础上,实现这个要求就很简单了
在拿到各层节点值的 list 后,按头插的方式,插入链表 list 中,就实现了自底向上的层序遍历了(2023/09/09晚)
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static List<List<Integer>> levelOrderBottom (TreeNode root) { List<List<Integer>> levelOrder = new LinkedList <List<Integer>>(); if (root == null ) { return levelOrder; } Queue<TreeNode> queue = new LinkedList <TreeNode>(); queue.offer(root); while (!queue.isEmpty()) { List<Integer> level = new ArrayList <Integer>(); int size = queue.size(); for (int i = 0 ; i < size; i++) { TreeNode node = queue.poll(); level.add(node.val); TreeNode left = node.left, right = node.right; if (left != null ) { queue.offer(left); } if (right != null ) { queue.offer(right); } } levelOrder.add(0 , level); } return levelOrder; }
锯齿形层序遍历
1 2 3 4 5 6 7 二叉树如下: 3 / \ 8 9 / \ / 20 11 6 锯齿形遍历:3 9 8 20 11 6
在基本的层序遍历的基础上,如何实现锯齿形层序遍历?
这样的思路能否完成呢:其他条件不变,改变存放节点值的顺序 如何?(2023/09/10午)
我们这里将 list 该换为 queue ,因为它可以提供了不同的入队方式
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static List<List<Integer>> zigzagLevelOrder2 (TreeNode root) { List<List<Integer>> ans = new LinkedList <>(); if (root == null ) return ans; Deque<TreeNode> queue = new LinkedList <>(); queue.offer(root); boolean isOrderLeft = true ; while (!queue.isEmpty()) { Deque<Integer> levelList = new LinkedList <>(); int size = queue.size(); for (int i = 0 ; i < size; i++) { TreeNode poll = queue.poll(); if (isOrderLeft) { levelList.addFirst(poll.val); } else { levelList.addLast(poll.val); } if (poll.left != null ) { queue.add(poll.left); } if (poll.left != null ) { queue.add(poll.right); } } ans.add(new LinkedList <Integer>(levelList)); isOrderLeft = !isOrderLeft; } return ans; }
N叉数的层序遍历
问题:将二叉树的情景改为N叉数,遍历结果也是很简单的,即将左右子节点改为若干子节点(2023/09/10午)
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static List<List<Integer>> nLevelOrder (NTreeNode root) { List<List<Integer>> ans = new ArrayList <>(); Deque<NTreeNode> queue = new ArrayDeque <>(); if (root != null ) queue.addLast(root); while (!queue.isEmpty()) { Deque<NTreeNode> next = new ArrayDeque <>(); List<Integer> nd = new ArrayList <>(); while (!queue.isEmpty()) { NTreeNode cur = queue.pollFirst(); nd.add(cur.val); for (NTreeNode chd : cur.children) { if (chd != null ) next.add(chd); } } queue = next; ans.add(nd); } return ans; }
获取每一层的最大数(扩展)
出队每层节点时,累加节点值,求平均值即可:(2023/09/11早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static List<Double> averageOfLevels (TreeNode root) { List<Double> res = new ArrayList <>(); if (root == null ) return res; Queue<TreeNode> list = new LinkedList <>(); list.add(root); while (list.size() != 0 ) { int len = list.size(); double sum = 0 ; for (int i = 0 ; i < len; i++) { TreeNode node = list.poll(); sum += node.val; if (node.left != null ) list.add(node.left); if (node.right != null ) list.add(node.right); } res.add(sum / len); } return res; }
获取每一层的平均数(扩展)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static List<Double> largestValues2 (TreeNode root) { List<Double> res = new ArrayList <>(); if (root == null ) return res; Queue<TreeNode> list = new LinkedList <>(); list.add(root); while (list.size() != 0 ) { int len = list.size(); double max = Double.MIN_VALUE; for (int i = 0 ; i < len; i++) { TreeNode node = list.poll(); max = Double.max(max, node.val); if (node.left != null ) list.add(node.left); if (node.right != null ) list.add(node.right); } res.add(max); } return res; }
获取二叉树的右视图(扩展)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static List<Integer> rightSideView (TreeNode root) { List<Integer> res = new ArrayList <>(); if (root == null ) { return res; } Queue<TreeNode> queue = new LinkedList <>(); queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0 ; i < size; i++) { TreeNode node = queue.poll(); if (node.left != null ) { queue.offer(node.left); } if (node.right != null ) { queue.offer(node.right); } if (i == size - 1 ) { res.add(node.val); } } } return res; }
通关(过关挑战)
情景:获取一个二叉树的左视图(2023/09/12午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static List<Integer> rightSideView (TreeNode root) { List<Integer> res = new ArrayList <>(); if (root == null ) { return res; } Queue<TreeNode> queue = new LinkedList <>(); queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0 ; i < size; i++) { TreeNode node = queue.poll(); if (node.left != null ) { queue.offer(node.left); } if (node.right != null ) { queue.offer(node.right); } if (i == 0 ) { res.add(node.val); } } } return res; }
二叉树遍历(深度优先) 基础知识(青铜挑战)
实战训练(白银挑战) 递归实现二叉树的前、中、后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void preOrder (TreeNode root, List<Integer> res) { if (root == null ) { return ; } res.add(root.val); preOrder(root.left, res); preOrder(root.right, res); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void inOrder (TreeNode root, List<Integer> res) { if (root == null ) { return ; } inOrder(root.left, res); res.add(root.val); inOrder(root.right, res); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void postOrder (TreeNode root, List<Integer> res) { if (root == null ) { return ; } postOrder(root.left, res); postOrder(root.right, res); res.add(root.val); }
迭代实现二叉树的前、中、后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static List<Integer> preOrderTraversal (TreeNode root) { List<Integer> res = new ArrayList <Integer>(); if (root == null ) { return res; } Deque<TreeNode> stack = new LinkedList <TreeNode>(); TreeNode node = root; while (!stack.isEmpty() || node != null ) { while (node != null ) { res.add(node.val); stack.push(node); node = node.left; } node = stack.pop(); node = node.right; } return res; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static List<Integer> inorderTraversal (TreeNode root) { List<Integer> res = new ArrayList <Integer>(); Deque<TreeNode> stack = new LinkedList <TreeNode>(); while (root != null || !stack.isEmpty()) { while (root != null ) { stack.push(root); root = root.left; } root = stack.pop(); res.add(root.val); root = root.right; } return res; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static List<Integer> postOrderTraversal (TreeNode root) { List<Integer> res = new ArrayList <>(); if (root == null ) return res; Stack<TreeNode> stack = new Stack <>(); TreeNode node = root; while (!stack.isEmpty() || node != null ) { while (node != null ) { res.add(node.val); stack.push(node); node = node.right; } node = stack.pop(); node = node.left; } Collections.reverse(res); return res; }
通关(过关挑战)
理解中序遍历的递归过程,挺难的,努努力吧(2023/09/12午)
二叉树深度优先经典问题
这块儿内容我个人认为非常抽象,虽然重要,但我还是决定先跳过这部分了,脑壳疼(2023/09/12午)
最大深度问题 判断平衡树 最小深度 N叉数的最大深度 二分查找和搜索数 基础知识(青铜挑战)
了解顺序查找、以及二分查找的简单实现。经典的顺序查找:
简单的二分查找
简单的二分查找,具体代码如下:(2023/09/12午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static int binarySearch1 (int [] array, int low, int high, int target) { while (low <= high) { int mid = (low + high) / 2 ; if (array[mid] == target) { return mid; } else if (array[mid] > target) { high = mid - 1 ; } else { low = mid + 1 ; } } return -1 ; }
待优化有两点:解决溢出问题 、除法->移位(提高运算效率)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static int binarySearch1 (int [] array, int low, int high, int target) { while (low <= high) { int mid = low + ((high - low) >> 1 ); if (array[mid] == target) { return mid; } else if (array[mid] > target) { high = mid - 1 ; } else { low = mid + 1 ; } } return -1 ; }
上面提到的二分查找是最常用的循环法 ,还有一种递归法: (2023/09/12午 )
二分查找(递归法) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static int binarySearch2 (int [] array, int low, int high, int target) { if (low < high) { int mid=low + ((high-low) >> 1 ); if (array[mid] == target) { return mid; } if (array[mid] > target) { return binarySearch2(array, low, mid - 1 , target); } else { return binarySearch2(array, mid + 1 , high, target); } } return -1 ; }
含重复元素的二分查找
思路:
找到指定元素 target 后,继续向左查找,mid–,
直到到达数组左边界,或者 nums[mid] 不等于 target 为止
返回 nums[mid++]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static int search1 (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 ; if (nums[0 ] == target) { return 0 ; } int right = nums.length - 1 ; while (left <= right) { int mid = left + (right - left) / 2 ; if (nums[mid] < target) { left = mid + 1 ; } else if (nums[mid] > target) { right = mid - 1 ; } else { while (mid != 0 && nums[mid] == target) mid--; return mid + 1 ; } } return -1 ; }
实战训练(白银挑战) 山脉数组峰顶查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int findPeakElement (int [] arr) { int n = arr.length; if (n == 1 ) { return 0 ; } for (int i = 0 ; i < n - 1 ; ++i) { if (arr[i] > arr[i + 1 ]) { return i; } } return n - 1 ; }
旋转数组最小数字 1 2 3 输入: nums = [4,4,4,5,1,2,3] 输出: 1 原数组为[1,2,3,4,5] ,旋转3 次得到输入数组
最小值就在中轴线,仍然是考虑到了进行节点左右大小的比较,并且在二分结束之后,直接拿到 nums[low],即为最小值
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static int findMin (int [] nums) { int low = 0 ; int high = nums.length - 1 ; while (low < high) { int pivot = low + ((high - low) >> 1 ); if (nums[pivot] < nums[high]) { high = pivot; } else { low = pivot + 1 ; } } return nums[low]; }
有序数组缺失数字
简单,二分查找,当 nums[i] != i 时,就拿到了这个数字(2023/09/15晚)
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static int solve (int [] a) { int left = 0 ; int right = a.length - 1 ; while (left < right) { int mid = (left + right) / 2 ; if (a[mid] == mid) { left = mid + 1 ; } else { right = mid; } } return left; }
优化求平方根 快速排序 基础知识(青铜挑战)
快速排序通过多次比较与交换来完成排序。而这个过程又被分为了多次重复单趟排序,接下来我们先从每一趟的排序讲起。
快速排序思想很简单,也很重要:
在一个无序数组中取一个数key,每一趟排序的最终目的是:让key的左边的所有数小于key,key的右边都大于key(假设排升序)
接下来,我们对key的左右区间进行单趟排序,可以预见的是,每次排序都固定好了一个数。而当我们对细分的左右区间进行单趟排序,最终整个数组都会化为有序
最常用的快排实现方法:快慢指针法
取最左边的数为key
定义两个快慢指针,都从key的下一位往右走
fast每走一步判断一下它指向的元素是否小于key,若小于则交换fast和slow位置的元素,并且让slow向前走
直到fast走到底,结束循环
最后让slow和key位置的值交换,再返回key的位置
具体代码如下:(2023/09/19午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void quickSort (int [] nums, int start, int end) { if (start >= end) { return ; } int pivot = nums[start]; int slow = start; int fast = start + 1 ; while (fast <= end) { if (nums[fast] < pivot) { slow++; swap(nums, slow, fast); } fast++; } swap(nums, start, slow); quickSort(nums, start, slow - 1 ); quickSort(nums, slow + 1 , end); } private void swap (int [] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }
实战训练(白银挑战) 数组中K大的数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public static void quicksort (int [] nums, int left, int right) { int start = left; int end = right; int pivot = nums[(end + start) / 2 ]; while (start < end) { while (nums[start] > pivot) { start++; } while (nums[end] < pivot) { end--; } if (start >= end) { break ; } int temp = nums[start]; nums[start] = nums[end]; nums[end] = temp; if (nums[start] == pivot) { end--; } if (nums[end] == pivot) { start++; } } if (start == end) { start++; end--; } if (left < end) { quicksort(nums, left, end); } if (right > start) { quicksort(nums, start, right); } }
归并排序
好难好难,需要先理解思路,再考虑编码实现:
归并排序简单来讲,就是将大的序列视为若干小的数组,利用归并思想实现排序的方法。
分治策略(分:把问题分成小的问题分别求解;治:将分的阶段得到的各答案合在一起)
位运算 基础知识(青铜排序)
原码、反码、补码
与运算 &,或运算 |,异或运算 ^,取反运算 !
移位运算:左移、算术右移、逻辑右移
移位运算与乘除法的关系
字符串 基础知识(青铜挑战) 转换成小写字母
遍历该字符串,依次转换为小写,存入新的字符串中
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 public static String toLowerCase (String s) { int n = s.length(); char [] chars = s.toCharArray(); for (int i = 0 ; i < n; ++i) { if (chars[i] >= 65 && chars[i] <= 90 ) { chars[i] += 32 ; } } String str = new String (chars); return str; }
实战训练(白银挑战) 反转字符串
思路:
使用双指针,一个在前,一个在后,相向移动,遍历字符串
在遍历的过程中,将两指针指向的元素交换位置
直到两指针相遇
具体代码如下:(2023/09/30早)
1 2 3 4 5 6 7 8 public static void reverseString (char [] s) { int n = s.length; for (int left = 0 , right = n - 1 ; left < right; ++left, --right) { char tmp = s[left]; s[left] = s[right]; s[right] = tmp; } }
K个一组反转
仅反转字母
我们有两种思路:
压栈法:
遍整个字符串,将字母依次压入栈中
创建一个新的字符串对象,再次遍历该字符串:如果是非字母,将该字符放入新字符串;如果是字母,则从存入弹出的栈顶元素
直到遍历结束
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static String reverseOnlyLetters (String S) { Stack<Character> letters = new Stack (); for (char c : S.toCharArray()) if (Character.isLetter(c)) letters.push(c); StringBuilder ans = new StringBuilder (); for (char c : S.toCharArray()) { if (Character.isLetter(c)) ans.append(letters.pop()); else ans.append(c); } return ans.toString(); }
双指针法:
定义两个指针,一个从头向后遍历,一个从后向前,维护字母字符
创建一个新的字符串对象,前指针遍历字符串
如果是非字母,则将该字符放入新字符串;如果是字母,则移动后指针,找到字母,将该字母存入新的字符串
直到前指针遍历结束
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static String reverseOnlyLetters2 (String S) { if (S == null || S.length() == 0 ){ return S; } StringBuffer ans = new StringBuffer (); int j = S.length() - 1 ; for (int i = 0 ; i < S.length(); i++) { if (Character.isLetter(S.charAt(i))){ while (!Character.isLetter(S.charAt(j))) j--; ans.append(S.charAt(j--)); }else { ans.append(S.charAt(i)); } } return ans.toString(); }
反转字符串里的单词
可以使用语言提供的方法解决:
将字符串按空格分开,转换为数组/集合
使用 reserve 方法,反转数组/集合元素
再将数组/集合转换为字符串
具体代码如下:(2023/09/30早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static String reverseWords2 (String s) { if (s == null || s.length() == 0 ) { return s; } s = s.trim(); String[] split = s.split("\\s+" ); List<String> wordList = Arrays.asList(split); Collections.reverse(wordList); return String.join(" " , wordList); }
验证回文串
1 2 A man, a plan, a canal: Panama 将该字符串反转后: amanaplanacanalpanama,这就是回文字符串
我们仍然使用双指针法解决:
首先要做的就是遍历一遍字符串,把所有的字母都放到新的字符串中
再使用双指针相向遍历新字符串,遍历的的过程中,判断两指针指向元素是否相同
直到两指针相遇,说明是回文字符串
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 public static boolean isPalindrome (String s) { StringBuffer sgood = new StringBuffer (); int length = s.length(); for (int i = 0 ; i < length; i++) { char ch = s.charAt(i); if (Character.isLetterOrDigit(ch)) { sgood.append(Character.toLowerCase(ch)); } } StringBuffer sgood_rev = new StringBuffer (sgood).reverse(); return sgood.toString().equals(sgood_rev.toString()); }
字符串里的第一个唯一字符
最好用的方法:Hash法
定义一个 Map 集合,我们遍历一遍字符串,使用 Map 记录每个字符出现过的次数
再次遍历该 Map,取到第一个出现次数大于1的元素
具体代码如下:(2023/09/30早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static int firstUniqChar (String s) { if (s == null || s.length() == 0 ) { return 0 ; } Map<Character, Integer> frequency = new HashMap <Character, Integer>(); for (int i = 0 ; i < s.length(); ++i) { char ch = s.charAt(i); frequency.put(ch, frequency.getOrDefault(ch, 0 ) + 1 ); } for (int i = 0 ; i < s.length(); ++i) { if (frequency.get(s.charAt(i)) == 1 ) { return i; } } return -1 ; }
判断是否互为字符重排
1 2 3 4 5 6 7 8 9 10 public static boolean checkPermutation (String s1, String s2) { char [] s1Chars = s1.toCharArray(); char [] s2Chars = s2.toCharArray(); Arrays.sort(s1Chars); Arrays.sort(s2Chars); return new String (s1Chars).equals(new String (s2Chars)); }
Map法:
分别统计两个字符串中,各个字符出现的次数
再次比较字符出现次数,只有每个字符出现的次数都一样,才能算两字符串互为字符重排
具体代码如下:(2023/09/30早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static boolean checkPermutation2 (String s1, String s2) { if (s1.length() != s2.length()) { return false ; } char [] s1chars = s1.toCharArray(); HashMap<Character, Integer> s1Map = getMap(s1); HashMap<Character, Integer> s2Map = getMap(s2); for (char s1char : s1chars) { if (!s2Map.containsKey(s1char) || (int ) s1Map.get(s1char) != (int ) s2Map.get(s1char)) { return false ; } } return true ; } private static HashMap<Character, Integer> getMap (String str) { HashMap<Character, Integer> map = new HashMap <>(); char [] chars = str.toCharArray(); for (char aChar : chars) { map.put(aChar, map.getOrDefault(aChar, 0 ) + 1 ); } return map; }
堆结构 基础知识(青铜挑战)
堆的概念 :堆是一种数据结构,按照完全二叉树的存储顺序,将数据存储在一个一维数组中
大顶堆 :任意节点值均大于它的左右节点值
小顶堆 :任意节点值均大于它的左右节点值
堆的构造过程 :按照层次将所有元素一次添入二叉树中,再不断调整,最终使其符合堆结构
堆中插入元素 :确认插入位置能够保持原二叉树为完全二叉树,再自底向上调整,保证每一层的子树都符合堆结构
堆中删除元素 :一般都是删除堆顶元素,将堆顶元素与二叉树最后一个元素对调,删除堆顶元素后,再依次调整每一层的各子树
实战训练(白银挑战) 数组中查找第K大的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static int findKthLargest (int [] nums, int k) { if (k > nums.length) { return -1 ; } int len = nums.length; PriorityQueue<Integer> minHeap = new PriorityQueue <>(k, (a, b) -> a - b); for (int i = 0 ; i < k; i++) { minHeap.add(nums[i]); } for (int i = k; i < len; i++) { Integer topEle = minHeap.peek(); if (nums[i] > topEle) { minHeap.poll(); minHeap.offer(nums[i]); } } return minHeap.peek(); }
堆排序
堆排序是什么原理呢?
构造大顶堆,依次取出堆顶元素;每取出一个堆顶元素后,重新调整堆,这样得到的序列就是从大到小排序的(降序排序)
小顶堆同理,得到的序列就是从小到大排序的(升序排序)
升序用小 ,降序用大 (2023/09/30晚)
合并K个有序链表 图算法 基础知识(青铜挑战)
有向图、无向图
边、弧
节点的度、入度、出度
描述节点之间的关系:<V1,V2> (V1,V2)
路径、回路
权、网
连通图、连通分量、强连通图
生成树、生成森林
实战训练(白银挑战) 图的存储 图的遍历 最小生成树问题 最短路径问题 排序算法
冒泡排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static int [] bubbleSort(int [] arr) { for (int i = 1 ; i < arr.length; i++) { boolean flag = true ; for (int j = 0 ; j < arr.length - i; j++) { if (arr[j] > arr[j + 1 ]) { int tmp = arr[j]; arr[j] = arr[j + 1 ]; arr[j + 1 ] = tmp; flag = false ; } } if (flag) { break ; } } return arr; }
选择排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static int [] selectionSort(int [] arr) { for (int i = 0 ; i < arr.length - 1 ; i++) { int minIndex = i; for (int j = i + 1 ; j < arr.length; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } if (minIndex != i) { int tmp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = tmp; } } return arr; }
插入排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static int [] insertionSort(int [] arr) { for (int i = 1 ; i < arr.length; i++) { int preIndex = i - 1 ; int current = arr[i]; while (preIndex >= 0 && current < arr[preIndex]) { arr[preIndex + 1 ] = arr[preIndex]; preIndex -= 1 ; } arr[preIndex + 1 ] = current; } return arr; }
快速排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public static void quickSort (int [] arr, int low, int high) { if (low >= high) { return ; } int point = arr[low]; int slow = low; int fast = low + 1 ; while (fast <= high) { if (arr[fast] < point) { slow++; int temp = arr[slow]; arr[slow] = arr[fast]; arr[fast] = temp; } fast++; } int temp = arr[slow]; arr[slow] = arr[low]; arr[low] = temp; quickSort(arr, low, slow - 1 ); quickSort(arr, slow + 1 , high); }
各排序算法比较
贪心
小试牛刀 分发饼干
你有几个孩子和几摞饼干,孩子有胃口,饼干有数量,尽可能分发饼干以满足胃口大的孩子
解题思路:
要尽可能满足胃口大的孩子,我们不妨把孩子们按胃口排序,从胃口最大的孩子开始,分发饼干
当然,与之对应的,每一摞饼干也要按照数量排好序
从胃口大的孩子开始,从数量多的饼干开始,依次给孩子分配
如果饼干不满足该孩子的胃口(此时饼干数量最多了),那就分配给下一个孩子(他的胃口更小),看能不能满足这个孩子的胃口
以此类推,饼干不满足孩子胃口,就找下一个孩子;满足了,再拿下一摞饼干进行分配
这样就能保证:能够合理分配这些饼干来尽可能满足胃口大的孩子 (2023/10/19晚)
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static int findContentChildren (int [] g, int [] s) { Arrays.sort(g); Arrays.sort(s); int count = 0 ; int start = s.length - 1 ; for (int index = g.length - 1 ; index >= 0 ; index--) { if (start >= 0 && g[index] <= s[start]) { start--; count++; } } return count; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static int findContentChildren (int [] g, int [] s) { Arrays.sort(g); Arrays.sort(s); int count = 0 ; int index = g.length - 1 ; int start = s.length - 1 ; while (index >= 0 ) { if (start >= 0 && g[index] <= s[start]) { start--; count++; } index--; } return count; }
柠檬水找零
情景:
你在卖柠檬水,一杯柠檬水 5 块钱,顾客排队购买你的柠檬水
每位顾客只买一杯,他们给你的金额有5块、10块和20块
最初你手头的余额为0,确保给每位顾客正确找零
解题思路:
顾客给你5块钱,你就照收不误
顾客给你10块钱,你得找他5块钱
顾客给你20块钱,你得找他5块钱和10块钱(优先),或者找他3张5块钱(其次)
也就是说,20块钱是不会用来找零钱的,我们就不关注手头20块钱的数目了
如果你找不了零钱,那一定是5块和10块其一不够找零钱了,我们只需关注这两种面值即可
确保给每位顾客正确找零,就要保证每处理一个顾客(每次找完零钱)后,手头的5块和10块不能“欠费”(2023/10/19晚)
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static boolean lemonadeChange (int [] bills) { int cash_5 = 0 ; int cash_10 = 0 ; for (int i = 0 ; i < bills.length; i++) { if (bills[i] == 5 ) { cash_5++; } else if (bills[i] == 10 ) { cash_5--; cash_10++; } else if (bills[i] == 20 ) { if (cash_10 > 0 ) { cash_10--; cash_5--; } else { cash_5 -= 3 ; } } if (cash_5 < 0 || cash_10 < 0 ) return false ; } return true ; }
分发糖果
情景:
你有几个孩子,糖果是无限制的,每个孩子都有各自的评分
要求根据他们的评分合理地给他们分发糖果 ,并要求用最少的糖果数完成
解题思路:
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static int candy (int [] ratings) { int [] candyVec = new int [ratings.length]; candyVec[0 ] = 1 ; for (int i = 1 ; i < ratings.length; i++) { if (ratings[i] > ratings[i - 1 ]) { candyVec[i] = candyVec[i - 1 ] + 1 ; } else { candyVec[i] = 1 ; } } for (int i = ratings.length - 2 ; i >= 0 ; i--) { if (ratings[i] > ratings[i + 1 ]) { candyVec[i] = Math.max(candyVec[i], candyVec[i + 1 ] + 1 ); } } int ans = 0 ; for (int s : candyVec) { ans += s; } return ans; }
高频问题 判断区间是否重叠
输入:intervals = [[0, 30], [15, 20], [5, 10]]
解释:存在重叠区间
详情如下图所示:
解题思路:(2023/10/22晚)
遍历每一个区间,相邻的两个区间作比较
如果前区间的尾部大于后区间的首部,就说明两区间重叠了
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 public static boolean canAttendMeetings (int [][] intervals) { Arrays.sort(intervals, (v1, v2) -> v1[0 ] - v2[0 ]); for (int i = 1 ; i < intervals.length; i++) { if (intervals[i][0 ] < intervals[i - 1 ][1 ]) { return false ; } } return true ; }
区间合并
输入:intervals = [[1, 3], [2, 6], [8, 10], [15, 18]]
输出:[[1, 6], [8, 10], [15, 18]]
解释:区间 [1, 3] 和 [2, 6] 重叠, 将它们合并为 [1, 6]
详情如下图所示:
解题思路(2023/10/22晚)
还是遍历每一个区间,判断相邻的两个区间是否重合
如果不重合,就继续向后遍历区间;如果重合,就合并两区间·
判断是否重合就能确定是否需要合并了,那么具体该如何合并呢?我们拿一个新的数组来维护合并区间后的新数组
合并:合并后的区间尾部取重合区间尾部的最大的那个,继续向后遍历区间
维护新的区间,一直比较新数组的尾部,直到不合并才开辟新区间(++idx)
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static int [][] merge(int [][] intervals) { Arrays.sort(intervals, (v1, v2) -> v1[0 ] - v2[0 ]); int [][] res = new int [intervals.length][2 ]; int idx = -1 ; for (int [] interval : intervals) { if (idx == -1 || interval[0 ] > res[idx][1 ]) { res[++idx] = interval; } else { res[idx][1 ] = Math.max(res[idx][1 ], interval[1 ]); } } return Arrays.copyOf(res, idx + 1 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Arrays.sort(intervals, (v1, v2) -> v1[0 ] - v2[0 ]); int [][] res = new int [intervals.length][2 ]; int idx = 0 ; if (intervals.length > 0 ) { res[idx] = intervals[0 ]; } for (int i = 1 ; i < intervals.length; i++) { if (intervals[i][0 ] > res[idx][1 ]) { res[++idx] = intervals[i]; } else { res[idx][1 ] = Math.max(res[idx][1 ], intervals[i][1 ]); } } return Arrays.copyOf(res, idx + 1 );
区间插入
输入:intervals = [[1, 3], [6, 9]], newInterval = [2, 5]
输出:[1, 5], [6, 9]
解释:新区间 [2, 5] 和 [1, 3] 重叠, 因此合并成为 [1, 5]
详情可参考下图:
解题思路(2023/10/22晚)
我们仍然拿一个新数组来维护结果集
遍历所有区间,将新区间左边且不重合的区间加入结果集(区间尾部小于新区间首部,就说明该区间在新区间的左边且不重合)
再次遍历所有区间,合并与新区间重合的区间(在经过第一轮遍历筛选后,这次遍历时,区间首部小于新区间尾部的都属于重合)
最后剩下的区间就属于新区间右边且不重合的区间了,直接挨个放进结果集就行
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static int [][] insert(int [][] intervals, int [] newInterval) { int [][] res = new int [intervals.length + 1 ][2 ]; int idx = 0 ; int i = 0 ; while (i < intervals.length && intervals[i][1 ] < newInterval[0 ]) { res[idx++] = intervals[i++]; } while (i < intervals.length && intervals[i][0 ] <= newInterval[1 ]) { newInterval[0 ] = Math.min(intervals[i][0 ], newInterval[0 ]); newInterval[1 ] = Math.max(intervals[i][1 ], newInterval[1 ]); i++; } res[idx++] = newInterval; while (i < intervals.length) { res[idx++] = intervals[i++]; } return Arrays.copyOf(res, idx); }
滑动窗口 高频问题 最小覆盖子串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 Map<Character, Integer> need = new HashMap <>(); Map<Character, Integer> window = new HashMap <>(); for (char c : t.toCharArray()) need.put(c, need.getOrDefault(c, 0 ) + 1 ); int left = 0 , right = 0 ; int valid = 0 ; int start = 0 , len = Integer.MAX_VALUE; while (right < s.length()) { char c = s.charAt(right); right++; if (need.containsKey(c)) { window.put(c, window.getOrDefault(c, 0 ) + 1 ); if (window.get(c).equals(need.get(c))) valid++; } while (valid == need.size()) { if (right - left < len) { start = left; len = right - left; } char d = s.charAt(left); left++; if (need.containsKey(d)) { if (window.get(d).equals(need.get(d))) valid--; window.put(d, window.get(d) - 1 ); } } } return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
字符串排列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 HashMap<Character, Integer> need = new HashMap <>(); HashMap<Character, Integer> window = new HashMap <>(); for (int i = 0 ; i < t.length(); i++) { char c = t.charAt(i); need.put(c, need.getOrDefault(c, 0 ) + 1 ); } int left = 0 , right = 0 ; int valid = 0 ; while (right < s.length()) { char c = s.charAt(right); right++; if (need.containsKey(c)) { window.put(c, window.getOrDefault(c, 0 ) + 1 ); if (window.get(c).equals(need.get(c))) valid++; } while (right - left >= t.length()) { if (valid == need.size()) return true ; char d = s.charAt(left); left++; if (need.containsKey(d)) { if (window.get(d).equals(need.get(d))) valid--; window.put(d, window.getOrDefault(d, 0 ) - 1 ); } } } return false ;
找所有字母异位词 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 Map<Character, Integer> need = new HashMap <>(); Map<Character, Integer> window = new HashMap <>(); for (int i = 0 ; i < t.length(); i++) { char c = t.charAt(i); need.put(c, need.getOrDefault(c, 0 ) + 1 ); } int left = 0 , right = 0 ; int valid = 0 ; List<Integer> res = new ArrayList <>(); while (right < s.length()) { char c = s.charAt(right); right++; if (need.containsKey(c)) { window.put(c, window.getOrDefault(c, 0 ) + 1 ); if (window.get(c).equals(need.get(c))) { valid++; } } while (right - left >= t.length()) { if (valid == need.size()) { res.add(left); } char d = s.charAt(left); left++; if (need.containsKey(d)) { if (window.get(d).equals(need.get(d))) { valid--; } window.put(d, window.get(d) - 1 ); } } } return res;
最长无重复字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Map<Character, Integer> window = new HashMap <>(); int left = 0 , right = 0 ; int res = 0 ; while (right < s.length()) { char c = s.charAt(right); right++; window.put(c, window.getOrDefault(c, 0 ) + 1 ); while (window.get(c) > 1 ) { char d = s.charAt(left); left++; window.put(d, window.get(d) - 1 ); } res = Math.max(res, right - left); } return res;