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 = null
return 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 = null
return 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);
//已经遍历到下一层的第一个节点,需要使当前层数加一进入下一层,并且当前该层遍历的节点数为 1
curlevel++;
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为本层的最后一个节点,那么下一层的最后一个节点肯定会在判断前赋值给nextcur
curlevelNodeNum++;
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)条件不违规的情况下,如果遇到了第一个左右子节点不全的,其后续节点不全都是叶子节点,返回false
if((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集合中对应的点所在的集合地址指向fromSet
for(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>() {
@Override
public 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>() {
@Override
public 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)) {
// 更新distance
distanceMap.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);
// 该弹出的点,即最小值的点索引记为-1
heapIndexMap.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) {
// 在堆中:曾经进来过,并且没有弹出
// 如果弹出了,就会将索引设置为-1
return 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.pass
index = 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>{
@Override
public 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>{
@Override
public int compare(Project o1, Project o2) {
return o1.cost - o2.cost;
}
}
//根据项目的利润建立的比较器(从大到小)
class ProfitComparator implements Comparator<Project>{
@Override
public 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>{
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
}
class GEComparator implements Comparator<Integer>{
@Override
public 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());
}
}
@Test
public 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位全是1
return 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到end
public 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)
);
}
}