2.1 哈希表、有序表、单链表和双链表

①哈希表的简单介绍

1>哈希表在使用层面可以理解为一种集合结构。

2>如果只有key,没有伴随数据value,可以使用HashSet结构

3>如果既有key,又有伴随数据value,可以使用HashMap结构

4>有无伴随数据,是HashSet和HashMap唯一区别,底层的实际结构是一回事

5>使用哈希表增(put)、删(remove)、改(put)和查(get)的操作,可以认为时间复杂度为O(1),但是常数时间比较大

6>放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小

    7>放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小(占用8byte

②有序表的简单介绍

1>有序表在使用层面上可以理解为一种集合结构

2>如果只有key,没有伴随数据value,可以使用TreeSet结构

3>如果既有key,又有伴随数据value,可以使用TreeMap结构

4>有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事

5>有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织

6>红黑树、AVL树,size-balance-tree和跳表都属于有序表结构,只是底层具体实现不同

7>放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小

8>放入哈希表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小

9>不管是什么底层具体实现,只要是有序表,都有一下固定的基本功能和固定的时间复杂度

有序表的固定操作:

1>void put(K key,V value):将一个(key,value)记录加入到表中,或者将key的记录更新成value

2>V get(K key):根据指定的key,查询value并返回

3>void remove(K key):移除key的记录

4>Boolean containKey(K key):询问是否有关于key的记录

5>K firstKey():返回所有键值的排序结果中,最左(小)的那个

6>K lastKey():返回所有键值的排序结果中,最右(大)的那个

7>K floorKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的前一个

8>K ceilingKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的后一个

以上所有操作时间复杂度都是O(logn),n为有序表含有的记录数

③单链表和双链表

单链表的节点结构

Class Node<V>{

V value;

Node next;//记录下一个节点

}

由以上结构的节点依次连接起来所形成的链叫单链表结构

双链表的节点结构

Class Node<V>{

V value;

Node pre;//记录上一个节点

Node next;//记录下一个节点

}

由以上结构的节点依次连接起来所形成的链叫双链表结构

单链表和双链表结构只需要给定一个头部节点head,就可以找到剩下的所有节点。

问题1:分别实现反转单向链表和双向链表的函数(要求时间复杂度为O(N),额外空间复杂度O(1)

/*** 反转单向链表和双向链表,要求时间复杂度为O(n),额外空间复杂度为O(1)*/
public class ReverseSingleAndDoubleLinkedList {class SingleNode{public int value;public SingleNode next;public SingleNode(int value){this.value = value;}
}class DoubleNode{public int value;public DoubleNode pre;public DoubleNode next;public DoubleNode(int value){this.value = value;}public void setPreAndNext(DoubleNode d1,DoubleNode d2){this.pre = d1;this.next = d2;}
}//反转单向链表public static SingleNode reverseSingleLinkedList(SingleNode head){if(head == null || head.next == null){return head;}SingleNode preNode = null;SingleNode nextNode = null;while (head != null){nextNode = head.next;//先保存下一个节点信息,防止断链head.next = preNode; //修改当前节点中的后驱节点内容,指向前驱节点preNode = head;//记录当前节点,并将当前节点作为下一个节点的前驱节点head = nextNode;}//结束后head指向原链表最后节点的下一个节点 head = nullreturn preNode;}//反转双向链表public static DoubleNode reverseDoubleLinkedList(DoubleNode head){if(head == null || head.next == null){return head;}DoubleNode temp = null;DoubleNode nextNode = null;while (head != null){//双向链表中直接将其中的pre和next互换nextNode = head.next;head.next = head.pre;head.pre = nextNode;//保存当前节点,用于最后返回;节点后移继续反转temp = head;head = nextNode;}//结束后head指向原链表最后节点的下一个节点 head = nullreturn temp;}
}

问题2:给定两个有序链表的头指针head1和head2,打印两个lianbiao 的公共部分(要求时间复杂度为O(n),额外空间复杂度O(1))

public class PrintCommom {class SingleNode{public int value;public SingleNode next;public SingleNode(int value){this.value = value;}}public static void printcommonPart(SingleNode d1,SingleNode d2){//用两个指针记录当前的位置SingleNode p1 = d1;SingleNode p2 = d2;while(p1 != null && p2 != null){//谁小谁移动,相等共同移动if(p1.value < p2.value){p1 = p1.next;}else if(p1.value > p2.value){p2 = p2.next;}else {System.out.println(p1.value);p1 = p1.next;p2 = p2.next;}}}
}

问题3:判断一个链表是否为回文链表(要求时间复杂度O(n),空间复杂度O(1))

方法一:使用额外数据结构——栈,将链表每个元素压栈,链表遍历完,重新遍历,每遍历一个元素,出栈一个元素,并进行对比。-----> 额外空间复杂度O(n)

//方法一:将全部元素压栈,然后依次出栈比照 --- 额外空间复杂度O(n)
public static boolean judgeLinkedListMethod1(SingleNode head){if(head == null || head.next == null){return true;}Stack<SingleNode> stack = new Stack<>(); //将每个链表元素压栈SingleNode temp = head;while(temp != null){//遍历过程中,头节点的位置会改变,需要一个临时的指针stack.push(temp);temp = temp.next;}//依次出栈,并比较while(!stack.empty()){if(head.value != stack.pop().value){return false;}head = head.next;}return true;
}

方法二:只将链表的一半压入栈,通过快慢指针找到中间位置,并把后半部分压栈,然后和链表前半部分比较 ---> 额外空间复杂度O(n/2)

//方法二:通过快慢指针,只存储链表中间向后的部分,入栈,然后出栈和链表前半部分比较---> 额外空间复杂度O(n/2)
public static boolean judgeLinkedListMethod2(SingleNode head){if(head == null || head.next == null){return true;}//定义快慢指针,快指针一次走两步,慢指针一次走一步//需要判断快指针是否为空SingleNode fast = head;SingleNode slow = head;//当fast.next.next == null 表明该链表为偶数个,现在fast指向最后一个节点的前一个节点位置,slow指向中间位置的前一个节点//当fast.next == null 表明链表为奇数个,现在fast指向最后一个节点位置,slow指向中间唯一一个节点位置while(fast.next != null && fast.next.next != null){fast = fast.next.next;slow = slow.next;}//将slow后面的节点全部入栈 --> 现在slow指向中间节点(奇数个)或者中间节点的前一个节点(偶数个)Stack<SingleNode> stack = new Stack<>();while(slow.next != null){stack.push(slow.next);slow = slow.next;}//出栈比较while (!stack.empty()){if(head.value != stack.pop().value){return false;}head = head.next;}return true;
}

方法三:通过快慢指针,找到中间位置,反转链表后半部分,进行比较。---> 额外空间复杂度O(1)

//方法三:将链表对折,后半部分的链表进行反转,进行比较.(注意在返回之前把链表回复原样)  --> 额外空间复杂度O(1)
public static boolean judgeLinkedListMethod3(SingleNode head){if(head == null || head.next == null){return true;}SingleNode cur = head; //指针,用于遍历比较SingleNode fast = head;SingleNode slow = head;while(fast.next != null && fast.next.next != null){fast = fast.next.next;slow = slow.next;}//反转中间后面的链表SingleNode end = reverseLinkedList(slow.next);//进行比较fast = end;while (cur != null && fast != null){if(cur.value != fast.value){//如果不是回文结构,也要先把链表恢复原来的样子cur = reverseLinkedList(end);return false;}cur = cur.next;fast = fast.next;}//将反转的链表结构修改回去slow.next = reverseLinkedList(slow.next);return true;
}public static SingleNode reverseLinkedList(SingleNode head){if(head == null || head.next == null){return head;}SingleNode next = null;SingleNode pre = null;while(head != null){next = head.next;head.next = pre;pre = head;head = next;}return pre;
}

问题4:将单向链表按某值划分为左边小、中间相等、右边大的形式

【进阶】在实现原问题功能的基础上增加如下的要求

1.调整后所有小于num的节点之间的相对顺序和调整前一样

2.调整后所有等于num的节点之间的相对顺序和调整前一样

3.调整后所有大于num的节点之间的相对顺序和调整前一样

4.时间复杂度达到O(n),额外空间复杂度达到O(1)

方法1:不考虑时间复杂度和额外空间复杂度,直接将链表元素存储再数组中,通过荷兰国旗排序方法进行分类,再将数组中元素一一组成链表--->时间复杂度O(n),额外空间复杂度O(n)

//方式一:不考虑时间复杂度和额外空间复杂度 ->将链表元素放入数组,然后排完序再放回来
public static SingleNode partitonMethod1(SingleNode head,int num){if(head == null || head.next == null){return head;}//计算节点个数,用于创建对应的数组长度SingleNode[] arr = null;int singleNodeLength = 0;SingleNode temp = head;while(temp != null){singleNodeLength++;temp = temp.next;}arr = new SingleNode[singleNodeLength];//把节点放入数组中temp = head;for(int i = 0;i < arr.length;i++){arr[i] = temp;temp = temp.next;}//数组元素按照给定值的大小进行分类heLanGuoQiSort(arr,0,arr.length-1,num);//按照排完序的数组进行链表连接for(int i = 0;i < arr.length-1;i++){arr[i].next = arr[i+1];}return arr[0];
}//排序方式 --> 按照荷兰国旗的方式
public static void heLanGuoQiSort(SingleNode[] arr,int L,int R,int num){int left = L - 1;int len = R - L + 1;int i = L;while(i < R && i < len){if(arr[i].value < num){swap(arr,i++,++left);}else if(arr[i].value > num){swap(arr,i,--R);}else {i++;}}
}

方法2:实现进阶的功能,通过六个变量定义小于num的上下界、等于num的上下界、大于num的上下界 ---> 时间复杂度O(n),额外空间复杂度O(1)

//方法二,通过六个变量,进行判断
public static SingleNode partitonMethod2(SingleNode head,int num){if(head == null || head.next == null){return head;}//定义六个变量,分别记录小于num链表的上下限,等于num链表的上下界,大于num链表的上下界SingleNode lt_num_head = null,lt_num_end = null,eq_num_head = null,eq_num_end = null,gt_num_head = null,gt_num_end = null;while(head != null){SingleNode temp = head.next;//保存下一个节点信息,用于后移遍历head.next = null;//将该节点与后面节点断链,否则将输出以该节点为头节点的链表信息if(head.value < num){if(lt_num_head == null){lt_num_head = lt_num_end = head;}else {//分段链表头不动,尾部添加新的元素,形成以分段链表的上界点为头节点的链表lt_num_end.next = head;lt_num_end = head;}}else if(head.value > num){if(gt_num_head == null){gt_num_head = gt_num_end = head;}else {gt_num_end.next = head;gt_num_end = head;}}else {if(eq_num_head == null){eq_num_head = eq_num_end = head;}else {eq_num_end.next = head;eq_num_end = head;}}head = temp;//指针后移继续遍历}//将这三段链表连接,注意空链表的存在if(lt_num_head != null){lt_num_end.next = eq_num_head;if(eq_num_head != null){eq_num_end.next = gt_num_head;}else {lt_num_end.next = gt_num_head;}return lt_num_head;}else {if(eq_num_head != null){eq_num_end.next = gt_num_head;return eq_num_head;}else {return gt_num_head;}}
}

问题5:复制含有随机指针节点的链表

一种特殊结构的单链表节点类描述如下:

class Node{

int value;

Node next;

Node rand;

Node(int val){value = val;}

}

rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。(要求时间复杂度O(n),额外空间复杂度O(1))

方法一:不考虑时间和空间复杂度

//方法一:直接复制,通过HashMap进行复制,先复制值,再复制其中的属性  --> 时间复杂度O(n),额外空间复杂度O(N)
public static SingleNode copyByHashMap1(SingleNode head){//通过引用外部数据结构进行复制操作HashMap<SingleNode,SingleNode> hashMap = new HashMap<>();SingleNode temp = head;//先创建相应的链表节点while(temp != null){hashMap.put(temp,new SingleNode(temp.value));temp = temp.next;}//复制其中的属性temp = head;while(temp != null){hashMap.get(temp).next = hashMap.get(temp.next);hashMap.get(temp).random = hashMap.get(temp.random);temp = temp.next;}return hashMap.get(head);
}

方法二:考虑时间和空间复杂度

//方法2:直接再原链表上进行复制,再分开  :1->1'->2->2'->3->3' 时间复杂度O(n),空间复杂度O(1)
public static SingleNode copyByHashMap2(SingleNode head){SingleNode temp = head;//在原链表的基础上构建 1->1'->2->2'->3->3'while(temp != null){SingleNode cur = temp.next;// 1->1'-2   复制当前节点,并且当前节点的下一个节点指向自己,复制的节点指向当前节点的下一个节点temp.next = new SingleNode(temp.value);temp.next.next = cur;temp = temp.next.next;}//现在新旧节点均在原来的链表上,将原链表节点的随机属性复制给新的节点temp = head;while(temp != null){temp.next.random = (temp.random == null) ? null : temp.random.next;temp = temp.next.next;}//分离出新节点,组成新链表SingleNode newhead = head.next;temp = head;while(temp != null){//可以先把下两个节点保存下来,//链表结构:1->1'->2->2'->3->3'SingleNode cur = temp.next; //先保存1‘temp.next = temp.next.next; //将 1-> 2 ,此时temp的下一个节点就是2   2->2'->3->3'//此时以cur为头节点的链表 : 1’->2->2'->3->3'cur.next = (cur.next == null) ? null : cur.next.next; //将 1'->2'temp = temp.next;//temp现在在节点1,应该向后指向2节点,由于上面已经将temp更新为 1->2->2'->3->3'}return newhead;
}

问题6:两个链表相交的一系列问题

给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null。时间复杂度O(n),额外空间复杂度O(1)

public class TwoSingleNodeIntersectProblems {static class SingleNode{int value;SingleNode next;public SingleNode(int value){this.value = value;}}public static SingleNode getIntersectNode(SingleNode head1,SingleNode head2){if(head1 == null || head2 == null){return null;}SingleNode loop1 = judgeSingleLinkedListisLoop2(head1);SingleNode loop2 = judgeSingleLinkedListisLoop2(head2);SingleNode res = null;if(loop1 == null && loop2 == null){//两个链表都没有环res = findIntersectPartsWithOutLoop(head1, head2);}else if(loop1 != null && loop2 != null){//两个链表都有环res = findIntersectPartsWithLoop(head1,loop1,head2,loop2);}else {//一个有环一个无环,肯定不相交res = null;}return res;}//方法一:判断一个单链表是否有环,使用HashSet来存储public static SingleNode judgeSingleLinkedListisLoop1(SingleNode head){if(head == null || head.next == null || head.next.next == null){return null;}//创建一个HashSet来存储链表节点,由于HashSet元素的不可重复性,可以判断HashSet<SingleNode> hashSet = new HashSet<>();while (head != null){if(!hashSet.contains(head)){//判断当前的节点在HashSet中是否已经有了,没有的话继续向下遍历,有就返回该节点hashSet.add(head);head = head.next;}else {return head;}}return null;}//方法二:使用快慢指针,当快慢指针相遇时,快指针再从头开始每次走一步,之后相遇的就是环的入口节点public static SingleNode judgeSingleLinkedListisLoop2(SingleNode head){if(head == null || head.next == null || head.next.next == null){return null;}SingleNode fast = null;//定义两个指针,快指针在没相遇前每次走两步,相遇后每次走一步,慢指针始终每次走一步SingleNode slow = null;while(fast != null && fast.next != null){fast = fast.next.next; //刚开始快指针每次走两步,慢指针每次走一步slow = slow.next;if(fast == slow){ //有环,快指针需要从头开始,快慢指针每次都走一步fast = head;while(fast != slow){fast = fast.next;slow = slow.next;}return fast;  //再次相遇的时候一定在第一个入环的节点处}}return null;}//两个无环单链表相交的话,他们的公共部分一定是到链表的结尾//先判断他们最后一个节点是否一致,一致则两个链表走差值步public static SingleNode findIntersectPartsWithOutLoop(SingleNode head1,SingleNode head2){if(head1 == null || head2 == null){return null;}SingleNode p1 = head1;//用两个指针记分别记录SingleNode p2 = head2;int count = 0; //记录两个链表之间的差值,链表1的长度-链表2的长度,可能为负值while (p1.next != null){  //这样,最后p1指向尾节点,不会指向空count++;p1 = p1.next;}while(p2.next != null){count--;p2 = p2.next;}if(p1 != p2){return null;}//链表长的引用给p1,短的给p2,谁长,谁先走 |count| 步p1 = count > 0 ? head1 : head2;p2 = p1 == head1 ? head2 : head1;count = Math.abs(count);//把count取为正数while(count != 0){count--;p1 = p1.next;}//长的链表走完差值过后,两个指针共同移动,这样他们相同是一定第一个相遇点while(p1 != p2){p1 = p1.next;p2 = p2.next;}return p1;}//一个有环,一个无环,在单链表的情况下肯定不会相交//两个都有环:①有环但不相交 ②在入环前相交  ③在入环后相交public static SingleNode findIntersectPartsWithLoop(SingleNode head1,SingleNode loop1,SingleNode head2,SingleNode loop2){//环外相交,相当于两个单链表无环的问题SingleNode p1 = null;SingleNode p2 = null;if(loop1 == loop2){p1 = head1;p2 = head2;int count = 0; //记录两个链表之间的差值,链表1的长度-链表2的长度,可能为负值while (p1.next != null){  //这样,最后p1指向尾节点,不会指向空count++;p1 = p1.next;}while(p2.next != null){count--;p2 = p2.next;}if(p1 != p2){return null;}//链表长的引用给p1,短的给p2,谁长,谁先走 |count| 步p1 = count > 0 ? head1 : head2;p2 = p1 == head1 ? head2 : head1;count = Math.abs(count);//把count取为正数while(count != 0){count--;p1 = p1.next;}//长的链表走完差值过后,两个指针共同移动,这样他们相同是一定第一个相遇点while(p1 != p2){p1 = p1.next;p2 = p2.next;}return p1;}else {p1 = loop1.next;while(p1 != loop1){ //在环内继续跑,如果在下次遇到自己之前没有找到,就没有相交节点if(p1 == loop2){return loop1;}p1 = p1.next;}return null;}}
}

2.2 二叉树

二叉树结点结构

class Node<V>{

V value;

Node left;

Node right;

}

用递归和非递归两种方式实现二叉树的先序、中序、后序遍历  (深度优先遍历)

public class TraverseBinaryTree {static class Node{int value;Node left;Node right;public Node(int value){this.value = value;}}//用递归的方式是实现二叉树的遍历public static void preOrder_Recursion(Node head) {//先序遍历if (head == null) {return;}//对于所有子树,先输出头结点,再左节点,最后右节点System.out.println(head.value);preOrder_Recursion(head.left);preOrder_Recursion(head.right);}public static void midOrder_Recursion(Node head){//中序遍历if(head == null){return;}midOrder_Recursion(head.left);System.out.println(head.value);midOrder_Recursion(head.right);}public static void posOrder_Recursion(Node head) {//后序遍历if(head == null){return;}posOrder_Recursion(head.left);posOrder_Recursion(head.right);System.out.println(head.value);}//非递归行为堆二叉树进行遍历  -- 采用栈//先序遍历:① 先把根节点压栈 ② 从栈中弹出一个cur ③ 处理cur ④ 先压入右孩子再压入左孩子 重复②③④public static void preOrder_NoRecursion(Node head){System.out.print("pre_Order: ");if(head != null){Stack<Node> stack = new Stack<>();stack.push(head);  //①while(!stack.isEmpty()){Node cur = stack.pop(); //②System.out.print(cur.value + "\t"); //③if(cur.right != null){ //④stack.push(cur.right);}if(cur.left != null){stack.push(cur.left);}}}}//中序遍历:① 每颗子树,整棵树的左边界进栈 ② 依次弹出,处理 ③ 对弹出节点的右子树重复步骤①②//public static void midOrder_NoRecursion(Node head){System.out.print("midOrder: ");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.print(head.value + "\t");head = head.right;}}}}//后序遍历:① 先把根节点压栈 ② 从栈中弹出一个cur,放入辅助栈中 ③ 先压入左孩子再压入右孩子 重复②③,④等到栈空,再依次处理辅助栈public static void posOrder_NoRecursion(Node head){System.out.print("posOrder: ");if(head != null) {Stack<Node> stack = new Stack<>();Stack<Node> curstack = new Stack<>();stack.push(head);  //①while (!stack.isEmpty()) {Node cur = stack.pop();  //②curstack.push(cur);if (cur.left != null) { //③stack.push(cur.left);}if (cur.right != null) {stack.push(cur.right);}}while (!curstack.isEmpty()) {//④System.out.print(curstack.pop().value + "\t");}}}
}

完成二叉树的宽度优先遍历(常见题目:求一课二叉树的宽度)

宽度遍历用队列(先进先出)

//宽度优先遍历:① 放入根节点 ② 弹出节点cur,处理 ③依次放入cur节点的左孩子和右孩子 ④ 重复②③操作
//队列是一个先进先出
public static void WidthFirstOrder(Node head){System.out.print("widthFirstOrder: ");if(head == null){return;}Queue<Node> queue = new LinkedList<>();queue.add(head);while (!queue.isEmpty()){Node cur = queue.poll();System.out.print(cur.value + "\t");if(cur.left != null){queue.add(cur.left);}if(cur.right != null){queue.add(cur.right);}}
}

问题:求一颗二叉树的最大宽度

//求一个二叉树的宽度   再宽度优先遍历上做修改,记录层数、每层与元素的个数
//方法一:采用哈希表记录节点和节点的层数
public static Integer widthOf(Node head){if(head == null){return 0;}Queue<Node> queue = new LinkedList<>();queue.add(head);HashMap<Node,Integer> hashMap = new HashMap<>(); //用来存放遍历的节点和节点所在的层数//定义两个变量用于记录当前查询所在的层数和所在层数节点个数hashMap.put(head,1);int curlevel = 1;int curlevelNodeNum = 0;int max = Integer.MIN_VALUE;//全局变量,保存某层最大的节点数while (!queue.isEmpty()){Node cur = queue.poll();if(curlevel == hashMap.get(cur)){curlevelNodeNum++;}else {//表明已经进入下一层的第一个节点,该层的节点数量已经得到,需要比对出最大值max = Math.max(max,curlevelNodeNum);//已经遍历到下一层的第一个节点,需要使当前层数加一进入下一层,并且当前该层遍历的节点数为 1curlevel++;curlevelNodeNum = 1;}if(cur.left != null){queue.add(cur.left);hashMap.put(cur.left,curlevel+1);}if(cur.right != null){queue.add(cur.right);hashMap.put(cur.right,curlevel+1);}}return max;
}//方法二:不使用哈希表,只采用有限几个变量
public static Integer widthOf2(Node head){if(head == null){return 0;}Queue<Node> queue = new LinkedList<>();queue.add(head);//定义三个变量Node curend = head;Node nextend = null;int curlevelNodeNum = 1;int max = Integer.MIN_VALUE;while(!queue.isEmpty()){Node cur = queue.poll();//先让当前节点的左孩子和右孩子进队列,再判断当前节点是否为本层的最后一个节点if(cur.left != null){queue.add(cur.left);nextend = cur.left;}if(cur.right != null){queue.add(cur.right);nextend = cur.right;}//如果当前的cur为本层的最后一个节点,那么下一层的最后一个节点肯定会在判断前赋值给nextcurcurlevelNodeNum++;if(cur == curend){ max = Math.max(max,curlevelNodeNum);curend = nextend;nextend = null;curlevelNodeNum = 0;}}return max;
}

二叉树的相关概念及其实现判断

① 如何判断一颗二叉树是否是搜索二叉树? 采用中序遍历的方式判断

搜索二叉树:每颗子树的左树都比头节点小,右数都比头节点大

//方法一:提供一个整体的全局变量,用于比较
public static int preValue = Integer.MIN_VALUE; //通过中序遍历的方式
public static boolean isBST(Node head){if(head == null){return true;}boolean isleftBST = isBST(head.left);if(!isleftBST){return false;}//逐层升序比较  (从最下面一层向上)if(head.value <= preValue){return false;}else {preValue = head.value;}return isBST(head.right);
}//方法二:使用递归套路解决问题
public static boolean isBinarySearchTree(Node head){return process(head).isBinarySearchTree;
}//需要知道左右子树的信息内容:左子树:①是否为搜索二叉树 ②当前搜索树的最大值
//                      右子树:①是否为搜索二叉树 ②当前搜索树的最小值
//构建返回参数信息
public static class ReturnType{boolean isBinarySearchTree;int min;int max;public ReturnType(boolean isBinarySearchTree,int min,int max){this.isBinarySearchTree = isBinarySearchTree;this.min = min;this.max = max;}
}public static ReturnType process(Node head){if (head == null){return null;}ReturnType left = process(head.left);ReturnType right = process(head.right);int min = head.value; //搜索二叉树的最小值经过比较肯定在左子树中int max = head.value; //搜索二叉树的最大值经过比较肯定在右子树中if(left != null){min = Math.min(min,left.min);max = Math.max(max,right.max);}if(right != null){min = Math.min(min,right.min);max = Math.max(max,right.max);}boolean isBinarySearchTree = true;if(left != null && (!left.isBinarySearchTree || left.min > head.value)){isBinarySearchTree = false;}if(right != null && (!right.isBinarySearchTree || right.max < head.value)){isBinarySearchTree = false;}return new ReturnType(isBinarySearchTree,min,max);
}

② 如何判断一颗二叉树是完全二叉树? 采用宽度优先遍历的方式

1)任一节点,有右孩子没有左孩子直接false

2)在1)条件不违规的情况下,如果遇到了第一个左右子节点不全的,其后续节点必须都要为叶子节点。

//采用宽度优先遍历的方式
public static boolean isCBT(Node head){if(head == null){return true;}Queue<Node> queue = new LinkedList<>();queue.add(head);//判断是否遇到过左右孩子不双全的节点,false表示双全,true表示不双全boolean flag = false;Node left = null;Node right = null;while(!queue.isEmpty()){Node cur = queue.poll();left = cur.left;right = cur.right;//判断当前节点是否为左右孩子双全的节点if(left == null || right == null){flag = true;}// 1) 任一节点,有右孩子没有左孩子直接false// 2) 在1)条件不违规的情况下,如果遇到了第一个左右子节点不全的,其后续节点不全都是叶子节点,返回falseif((flag && (left != null || right != null)) || (left == null && right != null)){return false;}if(left != null){queue.add(left);}if(right != null){queue.add(right);}}return true;
}

③ 如何判断一颗二叉树是否是满二叉树?

节点个数(N)和最大深度(L)满足 N=2^L-1

//左子树信息:①节点个数 ②左子树高度
//右子树信息:①节点个数 ②左子树高度
//节点个数(N)和最大深度(L)满足 N=2^L-1
public static boolean isFullBinaryTree(Node head){if(head == null){return true;}ReturnType p = process(head);return p.nodesNum == Math.pow(2,p.height) - 1;
}static class ReturnType{int height;int nodesNum;public ReturnType(int height,int nodesNum){this.height = height;this.nodesNum = nodesNum;}
}public static ReturnType process(Node head){if(head == null){return new ReturnType(0,0);}ReturnType left = process(head.left);ReturnType right = process(head.right);int height = Math.max(left.height,right.height) + 1;int nodesNum = left.nodesNum + right.nodesNum + 1;return new ReturnType(height,nodesNum);
}

④ 如何判断一颗二叉树是否是平衡二叉树? (递归套路)

对于任何一颗子树来说,左数和右树高度差不超过1

//那么对于左子树和右子树,我们需要知道什么条件
//左子树: ①是否为平衡二叉树 ②左子树的高度
//右子树: ①是否为平衡二叉树 ②左子树的高度
//把需要知道的信息内容当一个整体返回,构成递归public static boolean isbalancedTree(Node head){return process(head).isBalancedTree;
}static class ReturnType{boolean isBalancedTree;//是否为平衡二叉树int height;//树的高度public ReturnType(boolean isBalancedTree,int height){this.isBalancedTree = isBalancedTree;this.height = height;}
}public static ReturnType process(Node head){if(head == null){return new ReturnType(true,0);}ReturnType left = process(head.left); //左子树的情况ReturnType right = process(head.right); //右子树的情况int height = Math.max(left.height,right.height) + 1;boolean isbalancedTree = left.isBalancedTree && right.isBalancedTree&& Math.abs(left.height - right.height) < 2;return new ReturnType(isbalancedTree,height);
}

问题:给定两个二叉树的节点node1和node2,找到他们最低公共祖先节点

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;/*** 给定两个节点node1,node2,找他们的最低公共祖先*/
public class FindMinCommomAncestor {static class Node{int value;Node left;Node right;public Node(int value){this.value = value;}}//node1和node2一定属于一棵树//方法一:先建立head为根节点的所有子树父节点集合,再通过其保存node1的所有父节点,然后查询node2的父节点是否在node1的父节点中public static Node fmca(Node head,Node node1,Node node2){HashMap<Node,Node> fatherMap = new HashMap<>();fatherMap.put(head,head);//根节点的父节点是其自己process(head,fatherMap);//再保存树中所有的父节点Set<Node> set1 = new HashSet<>(); //保存node1向上的所有父节点Node cur = node1;while(cur != fatherMap.get(cur)){//只有到head时候才会停止set1.add(cur); //自己也要添加进去,很有可能自己是node2的父节点cur = fatherMap.get(cur);//向上回溯}cur = node2;while(cur != fatherMap.get(cur)){cur = fatherMap.get(cur);if(set1.contains(cur)){return cur;}}return head;}public static void process(Node head,HashMap<Node,Node> fatherMap){if(head == null){return;}fatherMap.put(head.left,head);fatherMap.put(head.right,head);process(head.left,fatherMap);process(head.right,fatherMap);}//方法二:public static Node findMinAncestor(Node head,Node node1,Node node2){if(head == null || head == node1 || head == node2){return head;}Node left = findMinAncestor(head.left, node1, node2);Node right = findMinAncestor(head.right, node1, node2);if(left != null && right != null){return head;}return left != null ? left : right;}
}

问题:在二叉树中找到一个节点的后继节点

有一个新型的二叉树节点类型如下:

public class Node{

int value;

Node left;

Node right;

Node parent;

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

}

该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假设有一棵Node类型的节点组成的二叉树,树中每一个节点的parent指针都正确的指向自己的父节点,头节点的parent指向null。只给一个在二叉树中的某个节点node,请实现返回node的后继节点的函数。

在二叉树的中序遍历中,node的下一个节点叫作node的后继节点。

//后继节点寻找分为两种情况
//① node 有右树时,后继节点为右树上最左的节点
//② node 无右树时,向上找,找到一个父亲的左树是node时,该父节点就是node的后继节点。该node节点是父节点左子树中的最右节点
public static Node findSuccessorNode(Node node){if(node == null){return node;}if(node.right != null){//node节点有右子树的情况//右树上最左的那个节点Node cur = node.right;while(cur.left != null){cur = cur.left;}return cur;}//node无右子树,则找到node节点的父节点Node parent = node.parent;while(parent != null && parent.left != node){node = parent;parent = node.parent;}return parent;
}

二叉树的序列化和反序列化(内存中二叉树如何变成字符串形式,又如何从字符串变为树)

//序列化  以先序遍历为例
public static String serialBinaryTree(Node head){if(head == null){return "#_";}String res = head.value + "_";res += serialBinaryTree(head.left);res += serialBinaryTree(head.right);return res;
}//反序列化 (怎么序列化的,就用什么方法反序列化)
//使用队列
public static Node returnNode(String s){//先把字符串的数据分割开来String[] strings = s.split("_");Queue<String> queue = new LinkedList<>();for(int i = 0;i < strings.length;i++){queue.add(strings[i]);}return preOrder(queue);
}public static Node preOrder(Queue<String> queue){String str = queue.poll();if("#".equals(str)){return null;}//先序遍历,那么就先建头节点,再建左子树,最后建右子树Node node = new Node(Integer.valueOf(str));node.left = preOrder(queue);node.right = preOrder(queue);return node;
}

折纸问题

/*** 折纸问题   中序遍历的方式*/
public static void printAllFolds(int N){printProcess(1,N,true);
}
//i是节点的层数,N是一共的层数,down == true为凹,down == false为凸
public static void printProcess(int i,int N,boolean down){if(i > N){return;}printProcess(i+1,N,true); //遍历左子树System.out.println(down ? "凹" : "凸");printProcess(i+1,N,false); //遍历右子树
}

2.3 图(构建自己熟悉的图结构,实现图的算法。在遇到不同的图结构只需要转换到自己熟悉的图即可)

图的存储方式:邻接表和邻接矩阵

/*** 自己设一个图的模板,可以把以后遇到不同的图结构转换成自己熟悉的图模板来实现算法*/
public class Graph {public HashMap<Integer,Node> nodes;  //存放图的点集public HashSet<Edge> edges;   //存放图的边的集合public Graph(){nodes = new HashMap<Integer, Node>();edges = new HashSet<Edge>();}
}class Node{public int value;   //图中该点的值public int in;      //进入该点的个数public int out;     //从该点出去的个数public ArrayList<Node> nexts;  //直接与该点相连的邻居(得是从该点出去的)public ArrayList<Edge> edges;  //从该点出去的边public Node(int value){this.value = value;this.in = 0;this.out = 0;this.nexts = new ArrayList<>();this.edges = new ArrayList<>();}
}class Edge{public int value;public Node from;  //出发点public Node to;    //终点public Edge(int value,Node from,Node to){this.value = value;this.from = from;this.to = to;}
}

例子:给一个图的结构,转换为自己熟悉的图结构

//matrix  所有的边
//N*3矩阵
//[weight,from节点上面的值,to节点上面的值]  【5,0,1】从0节点到1节点,长度为5
public static Graph creatGraph(Integer[][] matrix){Graph graph = new Graph();for(int i = 0;i < matrix.length;i++){Integer weight = matrix[i][0];  //获取到matrix上面的三个值Integer from = matrix[i][1];Integer to = matrix[i][2];if(!graph.nodes.containsKey(from)){ //没有出现过则新建点加入到点集nodes中graph.nodes.put(from,new Node(from));}if(!graph.nodes.containsKey(to)){graph.nodes.put(to,new Node(to));}//已经出现了,那么就要改变对应点集中对应点的属性(nexts、in、out、edges),图的边集也要添加这条边Node fromNode = graph.nodes.get(from);Node toNode = graph.nodes.get(to);Edge newEdge = new Edge(weight, fromNode, toNode);fromNode.nexts.add(toNode);fromNode.out++;toNode.in++;fromNode.edges.add(newEdge);graph.edges.add(newEdge);}return graph;
}

图的宽度优先遍历

①利用队列实现

②从源节点开始依次按照宽度进队列,然后弹出

③每弹出一个点,把该节点所有没进过队列的临界点放入队列

④知道队列变空

public static void bfs(Node node){if(node == null){return;}Queue<Node> queue = new LinkedList<>();HashSet<Node> hashSet = new HashSet<>();  //该set是为队列queue服务的,防止有重复的点进队列queue.add(node);hashSet.add(node);while (!queue.isEmpty()){Node cur = queue.poll();System.out.println(cur.value);//处理行为for(Node next : cur.nexts){  //通过点的邻居点进行遍历,过程中要防止重复的点进入队列即可if(!hashSet.contains(next)){ //如果已经包含之前的处理过的点,就跳过,加入下一个hashSet.add(next);queue.add(next);}}}
}

广度(深度)优先遍历

①利用栈实现

②从源节点开始把节点按照深度放入栈,然后弹出

③每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈

④直到栈边为空

public static void dfs(Node node){if(node == null){return;}Stack<Node> stack = new Stack<>();HashSet<Node> hashSet = new HashSet<>();stack.push(node);hashSet.add(node);System.out.println(node.value); //直接处理第一个点while (!stack.isEmpty()){Node cur = stack.pop();for(Node next : node.nexts){if(!hashSet.contains(next)){//将当前弹出栈的点和该点的邻居点都压入栈,保证这次寻找的路是一路到底的stack.push(cur);stack.push(next);hashSet.add(next);//对邻居点进行处理System.out.println(next.value);//每次只压入一组点和点的邻居,这样每次寻找都是一条路线break;}}}
}

拓扑排序算法

使用范围:要求有向图,且有入度(node.in)为0的节点,且没有环

/***算法步骤* ① 先找到入度为0的点,加入队列,从队列取出加入result集* ② 消除入度为0的点的影响(点和边的影响都需要消除)* ③ 寻找下一个入度为0的点,重复*/
public static List<Node> topolgicalSorted(Graph graph){//1.先遍历图中的所有点,将图的点和对应点的入度记录下来,并把入度为0的点放入队列HashMap<Node,Integer> inMap = new HashMap<>(); //点,点的入度Queue<Node> zeroInQueue = new LinkedList<>(); //只有入度为0的点才可以进入队列for(Node node : graph.nodes.values()){inMap.put(node,node.in);if(node.in == 0){  //第一次遍历图,碰到入度为0的点就加入到队列中zeroInQueue.add(node);}}//2.先将入度为0的点放入结果集,然后消除该点的影响(next.in),找入度为0点放入结果集.....List<Node> result = new ArrayList<>();while (!zeroInQueue.isEmpty()){Node cur = zeroInQueue.poll();result.add(cur);//消除该点的影响,该点的邻居点的入度都需要减一for(Node next : cur.nexts){inMap.put(next,inMap.get(next) - 1);if(inMap.get(next) == 0){zeroInQueue.add(next);}}}return result;
}

图生成最小生成树

连通图:在无向图中,若任意两个顶点都有路径相通,则称为该无向图为连通图

强连通图:在有向图中,若任意两个顶点都有路径相通,则称为该无向图为连通图

连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接两个顶点的代价,称这种连通图为连通网

生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一个有n个顶点的生成树有且仅有n-1条边,如果生成树中再添一条边,则必成环

最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树

求最小生成树的两种算法

①Kruskal算法(加边法):从边角度出发,先把边排序,依次选择最小边,如果加上这条边会形成环,则不加,如果不会形成换,则加上这条边。

/*** 最小生成树算法:Kruskal*  从边角度出发,先把边排序,从小到大*   依次选择最小的边,如果加上这条边,会形成环,则不加,不会形成环,则加*/
public class Kruskal {//如何判断图是否形成了环,就判断当前边的起点所在的集合和终点所在的集合是否是同一个集合//自定义一个结构,可以将图中的点集中的 点和当前点所在的集合储存起来public static class MySet{//存储该点以及该点所在的集合public HashMap<Node, List<Node>> setMap;public MySet(Collection<Node> nodes){ //初始化通过图的点集将每个点和每个点所在的集合放在hashmap中for(Node cur : nodes){List<Node> set = new ArrayList<>();set.add(cur);setMap.put(cur,set);}}//判断图中两个不同的点是否在一个集合中,在一个集合中那么他们集合的地址肯定时相同的public boolean isSameSet(Node from,Node to){List<Node> fromSet = setMap.get(from);List<Node> toSet = setMap.get(to);return fromSet == toSet;}//将图中两个不同的点加在一个集合里public void union(Node from,Node to){List<Node> fromSet = setMap.get(from);List<Node> toSet = setMap.get(to);//将toSet集合中的所有元素加入到fromSet中,并把toSet集合中对应的点所在的集合地址指向fromSetfor(Node toNode : toSet){fromSet.add(toNode);//更新这些点当前所在的集合setMap.put(toNode,fromSet);}}}//Kruskal算法public static Set<Edge> kruskalMST(Graph graph){//将图中的点集进行初始化MySet mySet = new MySet(graph.nodes.values());//创建优先级队列,放入边的信息,小根堆排序方式(从小到大)PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new Comparator<Edge>() {@Overridepublic int compare(Edge o1, Edge o2) {return o1.weight - o2.weight;}});//把图中的所有边按照大小放入for(Edge edge : graph.edges){priorityQueue.add(edge);}Set<Edge> result = new HashSet<>();while(!priorityQueue.isEmpty()){Edge edge = priorityQueue.poll();//判断当前弹出的边是否会构成环,不构成环就加入到结果集,并且将当前边的起点和终点放入一个集合if(!mySet.isSameSet(edge.from, edge.to)){result.add(edge);mySet.union(edge.from,edge.to);}}return result;}
}

②Prim算法(加点法):从随意一个点出发,从其邻居边集中寻找最小的边,判断选取的这条最小的边的另一点是否已经被选取,没有则选择,再从该点的邻居边集中选取最小的边,向外扩散。

public static Set<Edge> primMST(Graph graph){//创建一个优先级队列,排放解锁点的所有边PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new Comparator<Edge>() {@Overridepublic int compare(Edge o1, Edge o2) {return o1.weight - o2.weight;}});HashSet<Node> set = new HashSet<>(); //将解锁的边对应的点放入set集合,判断该点是否已经选择过了Set<Edge> result = new HashSet<>();  //将得到的边存放在结果集//遍历所有的点,是防止森林的情况,每次从一个点出发只会形成一棵树for(Node node : graph.nodes.values()) {if (!set.contains(node)) {  //表明该点还没有被选取过,可以选择set.add(node);for (Edge edge : node.edges) {  //把该点的所有邻居边都加到优先级队列priorityQueue.add(edge);}while (!priorityQueue.isEmpty()) {  //队列为空时,表明从node节点出发的最小生成树已经形成//弹出最小的一条边,并获得对应边的邻居节点Edge edge = priorityQueue.poll();Node toNode = edge.to;if (!set.contains(toNode)) {  //判断该邻居节点是否被重复选择,没有则添加对应的边,并且从该邻居节点向外扩散set.add(toNode);result.add(edge);for (Edge nextEdge : toNode.edges) { //从该邻居节点将所有的边加入到优先级队列priorityQueue.add(nextEdge);}}}}}return result;
}

Dijkstra算法(图中求最短路径算法):给定一个点,求其到其他节点的最短路径

使用要求:不可以有权值为负的边

public static HashMap<Node,Integer> dijkstra(Node head){//head  : 出发的节点//key : 从head节点出发到node节点//value :从head出发到node节点的最小距离     如果没有相应的节点信息,表示head节点到其距离为无穷大HashMap<Node,Integer> distanceMap = new HashMap<>();distanceMap.put(head,0);//表示锁住的节点,即head节点到其最短距离已经确定,后面不再更改HashSet<Node> selectedNodes = new HashSet<>();//从diatanceMap里找没有被锁住且离head最短距离的节点Node minNode = getMinDistanceAndUnselectedNode(distanceMap,selectedNodes);while(minNode != null){//先拿到head到minNode的距离Integer distance = distanceMap.get(minNode);//遍历minNode的边集,更新距离for(Edge edge : minNode.edges){//拿到对应边的to节点Node toNode = edge.to;//判断toNode节点是否记录在distanceMap,没有则添加,有则更新if(!distanceMap.containsKey(toNode)){//distanceMap中没有记录head到该节点的距离,则添加对应信息distanceMap.put(toNode,distance + edge.weight);}//判断已有的head->toNode节点的距离 和 head->minNode->toNode的距离之和   两者距离的大小distanceMap.put(toNode,Math.min(distanceMap.get(toNode),distance + edge.weight));}//将已使用的最小节点锁住,不再参与下次的最小节点选取selectedNodes.add(minNode); minNode = getMinDistanceAndUnselectedNode(distanceMap,selectedNodes); }return distanceMap;
}//寻找head节点到distanceMap已有节点(不包括被锁住的节点)中的最小路径的节点
public static Node getMinDistanceAndUnselectedNode(HashMap<Node,Integer> distanceMap,HashSet<Node> selectedNodesSet){//距离head最短路径的节点Node minNode = null;//最短距离int minDistance = Integer.MAX_VALUE;//在已有的点(已经在distanceMap中但没有被锁住的节点)中找 距离head节点最短路径的节点for(Map.Entry<Node,Integer> entry : distanceMap.entrySet()){Node node = entry.getKey();Integer diatance = entry.getValue();//寻找distanceMap中 没有被锁住但拥有到head节点最短距离的节点if(!selectedNodesSet.contains(node) && diatance < minDistance){minNode = node;minDistance = diatance;}}return minNode;
}

Dijkstra算法的优化:通过改写堆进行优化

public class Dijkatra_majorization {public static class NodeRecord {public Node node;public int distance;public NodeRecord(Node node, int distance) {this.node = node;this.distance = distance;}}public static class NodeHeap {private Node[] nodes;private HashMap<Node, Integer> heapIndexMap;// 节点在堆的位置private HashMap<Node, Integer> distanceMap;// 节点的值private int size; // 一共多少个节点public NodeHeap(int size) {nodes = new Node[size];heapIndexMap = new HashMap<>();distanceMap = new HashMap<>();this.size = 0;}public boolean isEmpty() {return size == 0;}public void addOrUpdateOrIgnore(Node node, int distance) {// 如果点在堆中,没有被弹出if (inHeap(node)) {// 更新distancedistanceMap.put(node, Math.min(distanceMap.get(node), distance));// 更新完之后,重新调整其在堆中的位置insertHeapify(node, heapIndexMap.get(node));}// 如果点不在堆中,也从来没有进来过,则新建记录// 如果点不在堆中,但进来过,就什么都不做,不执行下面代码if (!isEntered(node)) {// 新增这个点nodes[size] = node;heapIndexMap.put(node, size);distanceMap.put(node, distance);// 插入到应该有的位置insertHeapify(node, size++);}}public NodeRecord pop() {// 记录弹出的节点及其距离NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));// 交换最大值和最小值位置swap(0, size - 1);// 该弹出的点,即最小值的点索引记为-1heapIndexMap.put(nodes[size - 1], -1);// 移除该节点distanceMap.remove(nodes[size - 1]);// 移除该节点nodes[size - 1] = null;// 将最大值重新插入heapify(0, --size);return nodeRecord;}private void insertHeapify(Node node, int index) {while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {swap(index, (index - 1) / 2);index = (index - 1) / 2;}}private void heapify(int index, int size) {int left = index * 2 + 1;while (left < size) {int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])? left + 1 : left;smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;if (smallest == index) {break;}swap(smallest, index);index = smallest;left = index * 2 + 1;}}// node有没有进来过堆private boolean isEntered(Node node) {return heapIndexMap.containsKey(node);}// 点在不在堆中private boolean inHeap(Node node) {// 在堆中:曾经进来过,并且没有弹出// 如果弹出了,就会将索引设置为-1return isEntered(node) && heapIndexMap.get(node) != -1;}// 交换2个节点在堆中的位置private void swap(int index1, int index2) {// 先交换索引的位置heapIndexMap.put(nodes[index1], index2);heapIndexMap.put(nodes[index2], index1);// 再交换在堆中的位置Node tmp = nodes[index1];nodes[index1] = nodes[index2];nodes[index2] = tmp;}}// 对堆进行改进后的算法public static HashMap<Node, Integer> dijkstra2(Node head, int size) {// size:一共多少个点NodeHeap nodeHeap = new NodeHeap(size);// head到节点没有记录,则创造新记录// 如果有记录,则更新distance,如果值比原值要大,则不更新nodeHeap.addOrUpdateOrIgnore(head, 0);HashMap<Node, Integer> result = new HashMap<>();while (!nodeHeap.isEmpty()) {// 弹出最小值的点以及它的值NodeRecord record = nodeHeap.pop();Node cur = record.node;int distance = record.distance;// 遍历更新最小值的点延伸出去的边,更新其他点的路径for (Edge edge : cur.edges) {nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);}// 把不再更新的达到最小值路径的点保存result.put(cur, distance);}return result;}
}

前缀树

前缀树又名字典树,单词查找树,Tire树,是一种多路树形结构,是哈希树的变种,和hash效率有一拼,是一种用于快速检索的多叉树结构。

eg: 给出一组单词,abcd, abd, bcd, efg, hij,我们可以得到下面的Trie:

可以发现一些Tire树的特性:

1> 根节点不包含字符

2> 从根节点到某一节点的路径上的字符串连接起来,就是该节点对应的字符串

3> 每个节点的所有子节点包含的字符都不相同

注:每个节点都含有26个链接表示出现的26个小写字母,即每个节点表示的字符是26个字符中的一个,当字符串插入完成时,我们就会标记该字符串就是完整的字符串了。

因此,前缀树的节点定义如下:

/*** 前缀树的节点定义*/
class TrieNode{public int pass;  //表示经过该节点的次数public int end;  //表示其为尾节点的次数public TrieNode[] nexts;  //下一个节点集合,初始化会新建26个长度,对应位置表示'a' -'z'public TrieNode(){pass = 0;end = 0;//next[0] == null, 没有走向'a'的路//next[1] != null, 有走向'b'的路nexts = new TrieNode[26];//每个节点后面有26条路,指向26个字母,一开始全为null}
}

前缀树中有插入、删除、查询方法

/*** 前缀树*/
public class Trie {public TrieNode root; //定义一个根节点,之后的每个节点都需要从根节点出发public Trie(){root = new TrieNode();}//插入方法,将单词插入前缀树public void insert(String word){if(word == null){return;}char[] chars = word.toCharArray();  //将字符串转换为字符数组//新建指针,指向根节点,新插入的单词必须要经过根节点TrieNode node = root;node.pass++;int index = 0; //代表nexts[index]for (int i = 0;i < chars.length;i++){index = chars[i] - 'a';   //表示对应的字符为数组nexts中的哪一个 'a' -> 0;'b' -> 1 ..if(node.nexts[index] == null){ //当前节点没有去nexts[index]的路,就新建node.nexts[index] = new TrieNode();}//有路的话指针则来到next[index],并把对应的pass++node = node.nexts[index];node.pass++;}node.end++;  //该字串全插入过后,最后一个节点的end++}//查询方法,查询word单词之前加入过几次(怎么插入的,就怎么查询)  --> 依次查找到代表该单词最后一个字符的节点,查询其end的个数public int search(String word){if(word == null){return 0;}char[] chars = word.toCharArray();TrieNode node = root;int index = 0;for(int i = 0;i < chars.length;i++){  //根据单词遍历,找到该单词字符对应的最后一个节点,返回其end即可index = chars[i] - 'a';if(node.nexts[index] == null){return 0;}node = node.nexts[index];}return node.end;}//判断以 xxxx 作为字符串前缀的个数 --->  找到前缀字符串的最后一个字符对应的节点,得到经过该节点的数即可 即pass的值public int prefixNum(String pre){if(pre == null){return 0;}char[] chars = pre.toCharArray();TrieNode node = root;int index = 0;for(int i = 0;i < chars.length;i++){  //找到这个前缀字符串最后一个字符所对应的节点,得到经过该节点的个数即可node.passindex = chars[i] - 'a';if(node.nexts[index] == null){return 0;}node = node.nexts[index];}return node.pass;}//删除word字符串  根节点先减一,然后该单词每个字符对应的节点的pass值减一,最后一个字符对应的节点的end和pass都要减一public void delete(String word){if(search(word) != 0){  //确定树中有这个单词,才进行删除char[] chars = word.toCharArray();TrieNode node = root;node.pass--;int index = 0;for(int i = 0;i <chars.length;i++){index = chars[i] - 'a';//只要遇到一个节点的pass为0,//意味着该节点以及后面的没有意义(就是说这个节点之后的节点都只有要删除的单词对应的节点,没有其他),直接标空即可if(--node.nexts[index].pass == 0){node.nexts[index] = null;return;}node = node.nexts[index];}node.end--;  //到最后一个字符对应的节点的end医药减一}}
}

2.4 贪心算法

在某一个标准下,优先考虑最满足标准的样品,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫做贪心算法。

也就是说,不从整体最优上加以考虑,所作出的是在某种意义上的局部最优解。

贪心算法在笔试时的解题套路:

① 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试

② 脑补出贪心策略A、策略B、策略C...

③ 用解法X和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确

④ 不要纠结贪心策略的证明

问题1:会议安排

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲场次最多,返回这个最多的宣讲场次。

public class GreedyAlgorithm_ConferenceProblems {class Conference{int start; //会议开始的时间int end;  //会议结束的时间public Conference(int start,int end){this.start = start;this.end = end;}}//建立比较器(根据会议结束的时间进行比较)class ConferenceCamparator implements Comparator<Conference>{@Overridepublic int compare(Conference o1, Conference o2) {return o1.end - o2.end;}}//conferences:需要安排的会议数组  timePoint 当前来到的时间点public int BestConferenceArrangement(Conference[] conferences,int timePoint){//先根据会议结束的时间进行排序,结束最早的在前Arrays.sort(conferences,new ConferenceCamparator());int MaxConferenceNum = 0;for(int i = 0;i < conferences.length;i++){//只有当前会议开始的时间比当前的时间点迟,才可以添加,添加完,当前时间点变为添加会议的结束时间if(conferences[i].start >= timePoint){MaxConferenceNum++;timePoint = conferences[i].end;}}return MaxConferenceNum;}
}

问题2:分金条

一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。

输入一个数组,返回分割的最小代价。

public class GreedyAlgoorithm_GoldSegmentationProblems {public int MinCostOfGoldSegmentation(int[] arr) {//建立小根堆,放入数组的元素(这样在优先级队列中,就会按照从小到大的排序方式)PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();for (int i = 0; i < arr.length; i++) {priorityQueue.add(arr[i]);}//依次弹出两个数,进行相加,在将相加的数放入小根堆,再弹出两个数相加....直到优先级队列只剩刚加入的一个数时退出int sum = 0;int cur = 0;while (priorityQueue.size() > 1) {cur = priorityQueue.poll() + priorityQueue.poll();sum += cur;priorityQueue.add(cur);}return sum;}
}

问题3:可以获得的最大赚钱数

正数数组costs(cost[i]代表i号项目的花费),正数数组profits(profits[i]表示i号项目在扣除花费之后还能挣到的钱),正数K(表示可以串行的最多K个项目),正数M(初始资金)。

说明:每做完一个项目,马上获得的收益,可以支持你去做下一个项目

求最大的赚钱数

public class GreedyAlgorithm_MultiProjectsProfitProblems {class Project{int profit;  //项目的利润int cost;  //项目的花费public Project(int profit,int cost){this.profit = profit;this.cost = cost;}}//根据项目的花费建立的比较器(从小到大)class CostComparator implements Comparator<Project>{@Overridepublic int compare(Project o1, Project o2) {return o1.cost - o2.cost;}}//根据项目的利润建立的比较器(从大到小)class ProfitComparator implements Comparator<Project>{@Overridepublic int compare(Project o1, Project o2) {return o2.profit - o1.profit;}}//K:代表串行的最大项目  M:代表初始资金 public int MaxMoneyOfMultiProject(int K,int M,int[] profit,int[] cost){//新建两个优先级队列,分别根据花费和利润建立小根堆和大根堆PriorityQueue<Project> costpriorityQueue = new PriorityQueue<>(new CostComparator());PriorityQueue<Project> profitpriorityQueue = new PriorityQueue<>(new ProfitComparator()); //存放解锁项目的利润排序 //先根据利润和花费建立对应的项目,并根据花费大小放入花费的优先级队列for(int i = 0;i < profit.length;i++){costpriorityQueue.add(new Project(profit[i],cost[i]));}//根据资金的大小进行解锁项目,把解锁的项目全都放入利润的大根堆中,从可使用项目中选取利润最大的一个项目for(int i = 0;i < K;i++){ //最多串行K个项目while(!costpriorityQueue.isEmpty() && costpriorityQueue.peek().cost <= M){profitpriorityQueue.add(costpriorityQueue.poll());}//如果在K个项目前,可解锁的项目已经没有了,则直接返回if(profitpriorityQueue.isEmpty()){return M;}//现在本金就是利润加上初始的M += profitpriorityQueue.poll().profit;}return M;}
}

问题:一个数据流中,随时可以取得中位数

public class Median {//从小到大排序的比较器class LEComparator implements Comparator<Integer>{@Overridepublic int compare(Integer o1, Integer o2) {return o1 - o2;}}class GEComparator implements Comparator<Integer>{@Overridepublic int compare(Integer o1, Integer o2) {return o2 - o1;}}public Double getMedian(int[] arr){if(arr == null){return null;}if(arr.length == 1){return (double)arr[0];}//分别建立大根堆和小根堆PriorityQueue<Integer> maxpriorityQueue = new PriorityQueue<>(new GEComparator());PriorityQueue<Integer> minpriorityQueue = new PriorityQueue<>(new LEComparator());//第一个数直接加入大根堆maxpriorityQueue.add(arr[0]);for(int i = 1;i < arr.length;i++){//判断其与大根堆的第一个元素的大小,大于则放在小根堆,小于等于则放入大根堆if(arr[i] > maxpriorityQueue.peek()){minpriorityQueue.add(arr[i]);}else {maxpriorityQueue.add(arr[i]);}//判断大根堆和小根堆的大小是否大于1,大于1则要将size大的那个堆的第一个元素放入另一个堆if(maxpriorityQueue.size() - minpriorityQueue.size() > 1) {minpriorityQueue.add(maxpriorityQueue.poll());}else if(minpriorityQueue.size() - maxpriorityQueue.size() > 1){maxpriorityQueue.add(minpriorityQueue.poll());}}//取中位数,判断多少个数if(arr.length % 2 == 0){return ((double)maxpriorityQueue.peek() + (double)minpriorityQueue.peek()) / 2;}else {return (double) (maxpriorityQueue.size() > minpriorityQueue.size() ?maxpriorityQueue.peek() : minpriorityQueue.peek());}}@Testpublic void test(){int[] arr = {1,2,3,4,5,10};Double median = getMedian(arr);System.out.println("数组arr的median = " + median);}
}

N皇后问题(不能同行、不能同列、不能同对角线)

//方法一:用数组记录n皇后的位置public static int num1(int n){if(n < 1){return 0;}int[] record = new int[n];  //对应下标对应第几个皇后,对应值代表皇后摆放的位置return process1(0,record,n);}/**** @param i  表示目前来到第i行 ,表示现在存放的是 i+1 的皇后* @param record 只需要看 record[0...i-1]即可,前i个皇后存放的位置* @param n  一共放几个皇后* @return*/public static int process1(int i,int[] record,int n){if(i == n){return 1;}int res = 0; //记录有多少种摆法for(int j = 0;j < n;j++){ //表示现在第i行,从第0列一直到第n-1列,把所有的位置都判断一遍,//判断当前第i行的皇后,不能和之前(0....i-1)的皇后同列或者对角线if(isValid(record,i,j)){record[i] = j;res += process1(i+1,record,n);}}return res;}//判断第i行摆放的皇后,放在j列是否有效,即是否满足和之前(0...i-1)行的皇后不同列,不同对角线public static boolean isValid(int[] record,int i,int j){for (int k = 0;k < i;k++){if(j == record[k] || Math.abs(i - k) == Math.abs(j - record[k])){ //同列或者同对角线(行相减=列相减)return false;}}return true;}//方法二:位运算记录n皇后位置//不要超过32皇后问题public static int num2(int n){if(n < 1 || n > 32){return 0;}int limit = n == 32 ? -1 : (1 << n) - 1;  //限制条件,即n皇后申请一个二进制数,后n位全是1return process2(limit,0,0,0);}/*** 如果8皇后问题,现在有00010000(代表第四位放一个皇后),那么列限制=00010000,左斜限制=00100000,右斜限制=00001000* 那么一共的限制就是 00111000  即下一个皇后这些位置就不能填了* @param limit* @param colim  列限制,1的位置不能放皇后,0的位置可以放* @param leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以放* @param rightDiaLim  右斜线的限制,1的位置不能放皇后,0的位置可以放* @return*/public static int process2(int limit,int colim,int leftDiaLim,int rightDiaLim){if(colim == limit){ //当列限制和n皇后的限制条件相同,即代表所有的皇后都填上return 1;}int pos = 0;int mostRightOne = 0;pos = limit & (~(colim | leftDiaLim | rightDiaLim)); //皇后可以填的位置int res = 0;while(pos != 0){mostRightOne = pos & (~pos + 1);  //提取出候选皇后状态的最右侧的 1,表示皇后存放pos = pos - mostRightOne;  //把这个已经存放过的位置减掉,在用于下一次的皇后选择res += process2(limit,colim | mostRightOne,(leftDiaLim | mostRightOne) << 1,(rightDiaLim | mostRightOne) >>> 1);}return res;}
}

2.5 暴力递归

1.把问题转化为规模缩小了的同类问题的子问题

2.把明确的不需要继续进行递归的条件(base case)

3.有当得到了子问题的结果之后的决策过程

4.不记录每一个子问题的解

一定要学会怎么去尝试,因为这是动态规划的基础。

问题1:汉诺塔问题

打印n层汉诺塔从左边移动到最右边的全部过程

public class HanoiTower {//i:汉诺塔的层数//分三步 : 1~i-1 先从from 到 other;i从from到end;1~i-1从other到endpublic static void func(int i,String start,String end,String other){if(i == 1){System.out.println("Move 1 from " + start + " to " + end);}else {func(i - 1,start,other,end);System.out.println("Move " + i + " from " + start + " to " + end);func(i - 1,other,end,start);}}
}

问题2:打印一个字符串的全部子序列,包含空字符串

public class PrintAllSubString {public static void function(String str){char[] chars = str.toCharArray();process(chars,0,new ArrayList<>());}//i -> 当前来到i位置,对于该位置字符,选择要和不要,走两条路//res -> 之前进行选择,所形成的列表public static void process(char[] str, int i, List<Character> res){if(i == str.length){prinList(res);return;}//选择第i位的这个字符 List<Character> resChoose = copyList(res);resChoose.add(str[i]); //把当前i位的字符添加到字符数组中,继续向后选择process(str,i+1,resChoose);//不选择第i位的字符List<Character> resNotChoose = copyList(res);process(str,i+1,resNotChoose); //直接向后选择,不把当前i位的字符加入到字符数组中}public static void prinList(List<Character> res){for (int i = 0;i < res.size();i++){System.out.print(res.get(i));}}public static List<Character> copyList(List<Character> res){List<Character> copyList = new ArrayList<>();for (int i = 0;i < res.size();i++){copyList.add(res.get(i));}return copyList;}
}

问题3:打印一个字符串的全部排列

打印一个字符串的全部排列,要求不要出现重复的排列

public class PrintAllString {//给定一个字符串,获得其所有的字符排列成的字符串public static ArrayList<String> getAllString(String str){ArrayList<String> res = new ArrayList<>();if(str == null || str.length() == 0){return res;}char[] chars = str.toCharArray();process(chars,0,res);return res;}/**** @param str 已知的字符数组,在递归过程中会改变其中字符的位置,递归结束会还原* @param i   当前来到字符的位置,str[0....i-1]的字符会根据之前递归已经排好序,str[i...]后面字符的位置都可以出现在i的位置上* @param res  存放排好序的字符数组组成的字符串* @return*/public static ArrayList<String> process(char[] str,int i,ArrayList<String> res){if(i == str.length){res.add(String.valueOf(str));  //base case 当当前改变字符的位置已经和字符数组长度相同,表明已经排好了一种方式}boolean[] visit = new boolean[26]; //建立每个字符是否被访问//i位置前面的已经排好,不用管,只需要管i位置后面的字符for(int j = i;j < str.length;j++){if(!visit[str[j] - 'a']) {visit[str[j] - 'a'] = true;swap(str, i, j);//j位置的字符放在i的位置上process(str, i + 1, res);swap(str, i, j); //i位置和j位置交换,在这种方式排好序之后,把字符数组恢复}}return res;}public static void swap(char[] str,int i,int j){char t = str[i];str[i] = str[j];str[j] = t;}
}

问题4:手牌游戏

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家只能拿走最左或者最右的纸牌。请返回最后获胜者的分数。

public class CardGame {public static int winNum(int[] arr){if(arr == null || arr.length == 0){return 0;}return Math.max(f(arr,0,arr.length-1),s(arr,0,arr.length-1));}//先手玩家public static int f(int[] arr,int i,int j){if(i == j){return arr[i];}//先手玩家在下一次的范围上相当于是后手玩家return Math.max(arr[i] + s(arr,i + 1,j),arr[j] + s(arr,i,j-1));}//后手玩家public static int s(int[] arr,int i,int j){if(i == j){return 0;}//先手玩家选完后,后手玩家就相当于变为了先手//由于后手玩家的选择是先手玩家决定的,当先手玩家选择最优时,后手玩家就的选择就变成最差的return Math.min(f(arr,i + 1,j),f(arr,i,j-1));}
}

问题5:逆序栈

给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数,如何实现?

public class ReverseStack {public static void reverseStack(Stack<Integer> stack){if(stack.isEmpty()){return;}int i = f(stack); reverseStack(stack);stack.push(i);}//将栈底元素弹出,但栈结构保持不变 public static int f(Stack<Integer> stack){int result = stack.pop();if(stack.isEmpty()){return result;}else {int last = f(stack);stack.push(result);return last;}}
}

问题6:数字字符转化位字符串str

规定1和A相应,2和B相应,3和C相应......

那么一个数字字符串比如”111”,就可以转换为”AAA”、”KA”、”AK”

给定一个数字字符串,返回有多少种转化结果。

public class NumberStringToCharacter {public static int process(char[] str,int i){if(i == str.length){return 1;}if(str[i] == '0'){return 0;}if (str[i] == '1'){int res = process(str,i+1);//当前i位字符作为单独部分,后续有多少种方法if(i + 1 < str.length){res += process(str,i+2);  //(i和i+1)作为单独部分,后续有多少种方法}return res;}if(str[i] == '2'){int res = process(str,i+1);if(i + 1 < str.length && (str[i+1] >= '0' && str[i+1] <= '6')){res += process(str,i+2);}return res;}//i位置字符 '3' ~ '9' 只能作为单独部分,不能和后一个字符共同作为单独部分return process(str,i+1);}
}

问题7:袋子装最多价值

给定两个长度都为N的数组weights和values,weight[i]和value[i]分别代表i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?

public class BagWeightValue {/**** @param weights  货物重量数组* @param values   货物价值数组* @param i        当前来到哪一位货物* @param alreadyweight    目前袋中的货物价值* @param bag    袋子能承受的货物重量* @return*/public static int getMaxValue(int[] weights,int[] values,int i,int alreadyweight,int bag){if(alreadyweight > bag){return 0;}if(i == weights.length){return 0;}//第i号货物,选择要还是不要,要则加上相应的价值和重量return Math.max(getMaxValue(weights,values,i+1,alreadyweight,bag),values[i] + getMaxValue(weights,values,i+1,alreadyweight + weights[i],bag));}
}

左神数据结构与算法(基础)——表、树、图、贪心、递归相关推荐

  1. 【数据结构与算法基础】树与二叉树的互化

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  2. 左神数据结构与算法 笔记(二)

    文章目录 第七节课 1.二叉树 1.1.二叉树的先序.中序.后序遍历 1.2二叉树的按层遍历 二叉树的最大宽度: 1.3二叉树的序列化和反序列化 第八节课 1.题目一 2.题目二 3.折纸 4.二叉树 ...

  3. 【数据结构与算法基础】树的应用

    写在前面 树这一数据结构学的差不多了,该拉出来练练了.本节学习几个树的应用,包括优先队列.Huffman编码等. 1.优先队列(Priority Queue) 优先队列是特殊的"队列&quo ...

  4. 左神数据结构与算法(中级提升)——02

    题目十一:二叉树递归套路 二叉树的每个节点都有一个int型权值,给定一棵二叉树,要求计算出从根节点到叶节点的所有路径中,权值和 最大的值为多少. package class02;public clas ...

  5. (Java)算法基础6:图/贪心算法(带模板上考场,模板一定滚瓜烂熟解决考场订制)

    图由点集和边集构成. 有向图有箭头如下下图,无向图无箭头如下图 邻接表,如下图,记录ABCD的直接邻居. 这种结构可以表达所有图,比如有权值的图,如下下图 邻接矩阵法:用一个矩阵来表达上图(有向图,无 ...

  6. 【数据结构与算法基础】哈夫曼树与哈夫曼编码(C++)

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  7. 数据结构和算法基础--线性表

    数据结构和算法基础–线性表 数据结构 = 数据的逻辑结构+数据的存储结构+数据的运算 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-28ek7MfI-164242629 ...

  8. 数据结构与算法基础-学习-19-哈夫曼解码

    一.个人理解 哈夫曼树和哈夫曼编码相关概念.代码.实现思路分享,请看之前的文章链接<数据结构与算法基础-学习-17-二叉树之哈夫曼树>.<数据结构与算法基础-学习-18-哈夫曼编码& ...

  9. 数据结构与算法基础(java版)

    目录 数据结构与算法基础(java版) 1.1数据结构概述 1.2算法概述 2.1数组的基本使用 2.2 数组元素的添加 2.3数组元素的删除 2.4面向对象的数组 2.5查找算法之线性查找 2.6查 ...

最新文章

  1. 2018-3-27 遗传算法中的轮盘赌
  2. linux 内核3.1,NVIDIA发布了新的Tegra Linux开发包,内核为3.1.x
  3. 【跃迁之路】【497天】程序员高效学习方法论探索系列(实验阶段254-2018.06.17)...
  4. mysql中的乐观锁_MySQL中悲观锁和乐观锁到底是什么?
  5. ezdpl Linux自动化部署实战
  6. 排序算法Java实现(快速排序)
  7. 软件基本功:不要给代码加系数
  8. SQL SERVER 2005 批量收缩数据库
  9. 工程计算——实战:追赶法扰动分析
  10. 华为安装gsm框架_华为gms框架app下载-华为gms框架2020版下载最新版-乐游网安卓下载...
  11. 家庭版win7怎么把计算机,win7系统旗舰版如何变回家庭版
  12. c语言-输出菱形图案
  13. suffix tree学习
  14. Win11修改Hosts文件无法保存怎么解决?
  15. 物联网示范项目优秀案例集
  16. 图解CAN与CANopen协议,小白都能一目了然
  17. 2022全年度平板电视十大热门品牌销量榜单
  18. 《统计学习方法》 第十七章 潜在语义分析
  19. 提供云媒体服务器图片,云开发 把媒体文件上传到微信服务器 已知报错
  20. shell查找html里的ip,《通过脚本查看哪些ip被占用》shell笔记

热门文章

  1. 目标检测舰船数据集整合
  2. 超声波洗碗机控制器电源发生器设计
  3. 内网linux环境配置yum源
  4. Overture如何设置四手联弹的乐谱
  5. 《飞猪规则》 第八章 国外景点门票类商品发布规范
  6. 一探究竟:安信可模组ESP32-SU、ESP32-SL和ESP32-S对比,区别在哪里?
  7. dedecms后台报错“Undefined variable cfg_domain_cookie”的解决方法
  8. 普通人如何通过抖音赚钱?
  9. CMD中的一些Python操作:新建文件等
  10. Linux 文件系统怎么转换为8e类型