数据结构与算法入门(follow 左神)

本文是跟着左程云老师在b站发布的视频教程所记录的算法入门笔记,左神算法班
总共分为八节,一些相关代码和重点思路分析 大多已在下文呈现

一. 认识时间复杂度和简单排序算法

1.以选择排序为例

第一遍的时候,查询了n次,比较了n次,交换了1次;第二遍,查询n-1次,比较了n-1次,交换1次;。。。所以时间复杂度是O(n) ^2

算法分析先看时间复杂度指标,再分析不同数据样本下的实际运行时间,也就是"常数项时间"
额外空间复杂度指的是除去存放必要数据外,所需的空间。

2.异或运算

不同为1,相同为0;同时可以理解成无进位相加

 1)0^N=N; N^N=0
 2)交换律:a^b = b^a;结合律 (a^b) ^c= a^(b ^c)
第一题:已知一个数组中,只有一种数出现了奇数次,其余都是出现偶数次,请问怎么找到这种数
第二题:如果两种数出现了奇数次呢,怎么找到? 要求O(N)时间,O(1)空间。

第一题int eor=0和数组中全部异或得到的数就是结果,因为偶数次的结果都没了;相当于消消乐
第二题先按照第一题得到int eor=a^b; 由于a!=b,所以eor!=0
==> eor在32位比特位中至少有1位是1,假设是第8位,那么a和b的第八位一定一个是1,一个是0,不然得不到1这个异或结果
==> int eor2去异或数组中所有第8位不是1/不是0的数,最后结果一定是a or b;相当于将整个数组根据第8位是否是1分成两部分,这两部分分别包含a和b以及其他偶数次的数字
==> 最后再将eor^eor2得到另一个数
在这里插入图片描述

int rightOne = eor & (~eor  + 1); //提取最右侧的1  eor和eor的补码进行 同1得1,有0则0 的与运算
int onlyOne = 0;
for (int cur : arr) {
	if ((cur & rightOne) != 0)  //取出在最右侧1位置,数组中该位置也等于1的值做异或  /  ==0也行
	onlyOne ^= cur;
}
System.out.println(onlyOne + " " + (eor ^ onlyOne ));
3.插入排序

插入排序不是严格的O(N^2):0-1有序=>0-i有序,而冒泡和选择是。

    public static void insertionSort(int[] arr){
        if (arr=null || arr.length<2)
            return;
        for (int i=1;i<arr.length;i++)
            for (int j=i-1; j>=0 && arr[j]>arr[j+1] ;j--)//达成范围有序,往前看一旦碰上顺序 可跳下一循环
                    swap(arr,j,j+1);//相邻做交换
    }
4.二分查找

1>找一组有序数中≥某个数num的最小数(最左侧位置)
(1)判断mid是否≥num => 若≥num,标记t=mid,令end=mid-1找其左侧;
(2)左侧的mid若小于num,令start=mid+1,重新二分找;若此时mid>=num,由于该arr[mid]肯定小于t,重新令t=mid
(3)以此类推继续二分,就比正常二分多一个t标记。

2>局部最小值:在arr中,数值无序排列且相邻数一定不相等。局部最小指的是[0,N-1]中,arr[0]<arr[1],arr[N-1]<arr[N-2],arr[i-1]<arr[i]<arr[i+1],0<i<N-1,则这三个数局部最小。如果只求一个局部最小,请问时间复杂度能好于O(N)吗
可以,一开始比较start,end是否小于相邻数,都不行,此时这条函数曲线左边下降,右边也是下降;f(0)>f(1)且f(n-1)>f(n-2),中间必有转折点
则比较mid是否小于相邻数;如果此时不行,要么是f(m)>f(m-1)或f(m)>f(m+1),那就把f(m)当成新的start或end;当然f(m)是极大值,那两边都可
优化流程两个方向一是数据状况特殊,二是问题特殊
本题两个都特殊,数据相邻不等,问题求解时左右两边必和问题有关且必能舍弃一边就能采用二分策略

5.对数器

用于代替OJ检测
在这里插入图片描述
自己做的随机样本产生器产生的数据分别跑法a,法b,两相比较,若不一,二者至少有1出错

二. 认识O(NlogN)的排序

1.递归==栈的后序遍历

在这里插入图片描述
递归如果符合Master公式[算法导论主定理],可以直接用其计算时间复杂度
在这里插入图片描述

2.归并排序

准备一个辅助空间(空间复杂度O(N)),合并两个有序数组(各有一个min指针),使合并数组也有序
前面的排序浪费了大量的比较信息,而归并排序将比较行为变成了整体有序的部分去跟下一个更大范围的部分merge

    public static void process(int[] arr, int L, int R){
        if (L==R)
            return;
        int mid = L + ((R-L)>>1); //L+(R-L)/2,防止溢出
        process(arr,L,mid); //该递归时间复杂度按照Master公式,a=2,b=2,d=1;因此是O(NlogN)
        process(arr,mid+1,R);
        merge(arr,L,mid,R);
    }
    public static void merge(int[] arr, int L, int M, int R){
        int[] help = new int[R-L+1]; //辅助空间
        int i=0; //用于辅助空间的移动下标
        int p1=L, p2=M+1;
        while (p1<=M && p2<=R)
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        while (p1<=M)
            help[i++] = arr[p1++];
        while (p2<=R) //这两个while必执行一个
            help[i++] = arr[p2++];
        for (i=0;i<help.length;i++)
            arr[L+i] = help[i];
    }

扩展:

在这里插入图片描述
小和问题:
逆向思维反过来,求右边有几个数比它大(Merge),就产生几个这个数的小和。
只有左侧小,才会产生小和(此时乘以右侧排好序的数组剩余长度=>排序不能省略,通过排序知道右侧能产生几个小和),右侧小不产生小和
既不遗漏也不重算,因为merge过程中一定会把一个数扩到整体的,且变成merge变成整体后是不会在这个整体内部产生小和;
也就是说只有在merge时会产生小和,变成有序部分不会产生小和

public static int smallSum(int[] arr) {
		if (arr == null || arr.length < 2) {
			return 0;
		}
		return mergeSort(arr, 0, arr.length - 1);
	}
	public static int mergeSort(int[] arr, int l, int r) {
		if (l == r) {
			return 0;
		}
		int mid = l + ((r - l) >> 1);
		return mergeSort(arr, l, mid) 
				+ mergeSort(arr, mid + 1, r) 
				+ merge(arr, l, mid, r); // 左侧小和+右侧小和+两组归并新产生的小和
	}
	public static int merge(int[] arr, int l, int m, int r) {
		int[] help = new int[r - l + 1];
		int i = 0;
		int p1 = l;
		int p2 = m + 1;
		int res = 0;
		while (p1 <= m && p2 <= r) {
			res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0; //O(1)的时间找到r-p2+1个右侧产生小和的数【下标计算找】
			help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; //这里有修改,优先拷贝右侧到help【因为先拷贝左边,不知道右边有几个比左侧该值大的数(下标计算看不出来)】
		}
		while (p1 <= m) {
			help[i++] = arr[p1++];
		}
		while (p2 <= r) {
			help[i++] = arr[p2++];
		}
		for (i = 0; i < help.length; i++) {
			arr[l + i] = help[i];
		}
		return res;
	}

逆序对问题:和上面等效,只是左边比右边大而已。【找右边比左边小的元素】

res += arr[p1] > arr[p2] ? (m - p1 + 1): 0; //改这两句就可以
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]; //优先拷贝左侧【与上优先右侧同理】
3.快速排序

O(N^2)=>等差数列或者说partition一直离左侧或者右侧开头很近

在这里插入图片描述
第一题一次start和end的快排就解决
或者一开始i=0,小于等于区=null;如果arr[i]≤num,arr[i]和小于等于区下一个数交换,同时≤区右扩且i++
否则不右扩,i++,直到arr[i]≤num {快慢指针}
第二题则是两个区域,左边小于区域,右边大于区域
在这里插入图片描述
第三条情况i原地不动是因为,从右侧>区前一个数交换过来的数还没看过,得再拿他过一下情况1,2,3
当i == 大于区域时,停止
快排1.0 以最后一个数为num与大于5区域第一个数做交换,再在左右两侧继续之前行为
在这里插入图片描述
               在这里插入图片描述
在这里插入图片描述
      在这里插入图片描述
以荷兰国旗为例,一次搞定一批==5的数,比1.0稍快一些。
3.0采用概率随机选择索引范围内一个数做划分,做长期期望后为O(NlogN)

public static void quickSort(int[] arr, int l, int r) {
		if (l < r) {
			swap(arr, l + (int) (Math.random() * (r - l + 1)), r);//等概率随机选一个位置和最右位置交换
			int[] p = partition(arr, l, r); //返回数组长度为2=>划分中间区域的左边界和右边界
			quickSort(arr, l, p[0] - 1); // <区
			quickSort(arr, p[1] + 1, r); // >区
		}
	}
	// 默认以arr[r]做划分 arr[r]->p  |  <p    ==p     >p三块区域
	public static int[] partition(int[] arr, int l, int r) {
		int less = l - 1; //<区有边界
		int more = r;    //>区左边界
		while (l < more) {  //l表示当前数位置 arr[r]为划分值
			if (arr[l] < arr[r]) {
				swap(arr, ++less, l++);
			} else if (arr[l] > arr[r]) {
				swap(arr, --more, l); //>区左扩
			} else {
				l++; //中间区域不管
			}
		}
		swap(arr, more, r); //最后把划分值换过来,此时为中间位置最右边
		return new int[] { less + 1, more };
	}

快排的额外空间复杂度是O(logN):差的情况下,每次划分都是拿start和end做划分,且在递归过程中一直保留不释放=->O(N);好的情况是二叉树,排序一小部分,释放一部分=>O(logN)
即使是不用递归写,也要自行压栈,各种情况概率分布后会收敛到O(logN)

三.详解桶排序以及排序内容大总结

1.堆结构(优先级队列)

堆是完全二叉树,满足左右孩子分别是2i+1和2i+2,父节点是(i-1)/2
完全二叉树高度是O(logN),删掉最大值往下调整成堆也是O(logN)
假如有序是自小到大,堆排序就是构建大根堆,把构建好的堆最后一个位置与堆顶做交换,然后heapsize–,此时最后一位是最大值,然后对交换后的结构调整成堆,然后继续上述操作 逐步把大根堆顶放置最后一个(类似冒泡,但每次比较的值少了,且不额外空间复杂度O(1))。

public static void heapSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int i = 0; i < arr.length; i++) { //O(N)
			heapInsert(arr, i); // O(logN)
		}
		int size = arr.length;
		swap(arr, 0, --size); //最后一个数和堆顶做交换
		while (size > 0) { //周而复始 从最后一个排序到第一个 O(N)
			heapify(arr, 0, size); // O(logN)
			swap(arr, 0, --size); //O(1)
		}
	}
	//每次插入往上比:上
	public static void heapInsert(int[] arr, int index) {
		while (arr[index] > arr[(index - 1) / 2]) { //比父亲大,或当index=0时跳出    | (0 - 1) / 2 = 0, int自动抹去小数,-0.5=>0
			swap(arr, index, (index - 1) /2);
			index = (index - 1)/2 ;
		}
	}
	//任意位置往下堆化:下,例如删除堆顶后,将最后一个属放置在arr[0]时往下堆化
	public static void heapify(int[] arr, int index, int size) { //heapsize管着堆的大小
		int left = index * 2 + 1;
		while (left < size) { //下方还有(左)孩子
			int largest = left + 1 < size && arr[left + 1] > arr[left] 
					? left + 1 : left; //必须时有右孩子,且右孩子的值大于左孩子的值,右孩子才胜出
			largest = arr[largest] > arr[index] ? largest : index; //父和较大孩子间,谁值大,把下标给largest
			if (largest == index) {
				break;
			}
			swap(arr, largest, index);
			index = largest;
			left = index * 2 + 1;
		}
	}

如果只是给你一些数,只需求构建大根堆,不需要按照一个个往里填充的方式,没必要从后面的叶子节点开始调整,因为他们没有孩子,不需要heapify;所以从倒数第二排最后一个父节点开始向上调整(这样每个父亲节点只需要往下看就行)

for(int i = arr.length/2 -1 ;i>0; i--){
	heapInsert(arr, i);
}

扩展题
在这里插入图片描述
假设k=6,准备一个小根堆,遍历数组将前7个值放进小根堆,然后将堆顶对应的数组元素弹出放在arr[0]处,再把arr[7]放入小根堆,再弹出堆顶到arr[1] (类似滑动窗口,用小根堆找出)

PriorityQueue<Integer> heap = new PriorityQueue<>();

优先级队列(小根堆)底层是数组,但它扩容是成倍扩充 拷贝旧数组到新数组:单次扩容的代价由拷贝决定:O(N); 扩容次数是O(logN); N次均摊下来时间复杂度:O(NlogN)/N =O(logN)
系统提供代码是黑盒:只支持简单的add,poll,不支持自己随便修改堆中任意节点的值,不是做不了而是不够高效

2.比较器

c++:重载运算符
Java:比较两个函数/变量/属性/bean类等的大小:其实也是重载比较运算符

public static class Student {
		public String name;
		public int id;
		public int age;

		public Student(String name, int id, int age) {
			this.name = name;
			this.id = id;
			this.age = age;
		}
	}
	public static class IdAscendingComparator implements Comparator<Student> {
		//返回负数的时候,第一个参数排在前面 if(o1.id<o2.id) return -1;
		//返回正数的时候,第二个参数排在前面 if(o1.id>o2.id) return 1;
		//返回0,无所谓 return 0;
		@Override
		public int compare(Student o1, Student o2) {
			return o1.id - o2.id;
		}
		public static void main(String[] args) {
		Student student1 = new Student("A", 2, 23);
		Student student2 = new Student("B", 3, 21);
		Student student3 = new Student("C", 1, 22);

		Student[] students = new Student[] { student1, student2, student3 };
		Arrays.sort(students, new IdAscendingComparator()); //数组和怎么比大小的策略都给
		//如果不是自小到大,而是大到小,可以调整比较策略
		Interget[] arr = {5,4,3,2,7,9,0};
 		Arrays.sort(arr, new MyComp ());
	}
	public static class MyComp implements Comparator<Integer> {
		@Override
		public int compare(Integer o1, Integer o2) {
			return o2 - o1; //第二个参数➖第一个参数
		}
	}

同理,结合堆,自定义的时候,可以认为返回负数,认为第一个参数放在上面

PriorityQueue<Integer> heap = new PriorityQueue<>(new MyComp());
3.桶排序

之前所有的排序都是基于(数字)比较的排序

比如员工按照年龄排序,申请一个arr[200],下标对应员工年龄,arr[几]++,再把词频数组还原成 原年龄数组很容易;
其余呢,比较🐖,比较🐂呢,不能直接用排序算法,都得根据数据状况 在已有方法上改
基数排序(比较重要的不基于比较的排序)
先左边补齐0,再分别按照个十百千位 先进先出 的进桶出桶。就是分别按照个位,十位,百位排序
十进制准备十个桶,3进制准备三个桶

code:用不同的方法实现了桶的功能 不是全倒入桶再全倒出来,而是从右到左根据词频划出片(当前位有序)来
在这里插入图片描述
在这里插入图片描述
然后count [0,2,2,1,0,0…]=>[0,2,4,5,5,5…]变成前缀和数组 《=》这是由于从左到右先进先出现象可以看成从右到左后进后出
在这里插入图片描述
从右往左看的话,062是最右边的数,根据词频[0,2,4,5,5,5…],它应该在出桶的第4个,因此放在help[3]这里,同时词频减一变成[0,2,3,5,5,5…]
因为count[i]的值决定了在help数组中的index,也就是位置边界(比如52,62在排序后应该是help数组中的第3和第4个)

4.排序总结

稳定性的概念,即排序后相同值的数字(比较对象)的顺序是否和排序前数字(比较对象)顺序一致。
不过这个在计数的基础类型没啥用,结合上文的比较器在其他非基础类型有效(比如Mysql中数据先按姓名排,再按年龄排,需做到多条件排序依然有序)
(速)(希尔:多次插入)(择) 排序不稳定,另外的四大比较稳定(插入,冒泡,归并,基数):关键是相等时刻相对次序是否不变,其实只要不是相邻比较,做有跨度的交换就会破坏稳定
在这里插入图片描述

小结

1)基于比较的排序时间复杂度不能做到O(NlogN)以下
2)基于比较的排序在时间复杂度O(N
logN)时,不能做到空间复杂度O(N)以下还能保证稳定性
在这里插入图片描述
在这里插入图片描述
1)例如 小样本量(≤60)利用插入排序常数级低的优势,大样本量运用快排调度优势。
在这里插入图片描述
2)系统自定义的Arraysort 会根据你传参的数据类型选择快排(计数类型)还是归并排序(其他类型)
由于计数类型 稳定性无效,而其他类型为了保证稳定性选择归并。

四.链表

1.哈希表和有序表

在这里插入图片描述
上图的7) String虽然是引用数据类型, 但也按照key-value值传递,一律拷贝一份放到哈希表
而像新建一个Student类传入时,会拷贝其地址放入哈希表,一律8字节
在这里插入图片描述

TreeSet<Node> treeSet = new TreeSet<>(); //红黑树
// 以下的代码会报错,因为没有提供Node类型的比较器
try {
	treeSet.add(nodeA);
	treeSet.add(nodeC);
} catch (Exception e) {
	System.out.println("错误信息:" + e.getMessage());
}

treeSet = new TreeSet<>(new NodeComparator());
// 以下的代码没问题,因为提供了Node类型的比较器
try {
	treeSet.add(nodeA);
	treeSet.add(nodeB);
	System.out.println("这次节点都加入了");
} catch (Exception e) {
	System.out.println(e.getMessage());
}

// 展示有序表常用操作
TreeMap<Integer, String> treeMap1 = new TreeMap<>();
treeMap1.put(7, "我是7");
treeMap1.put(5, "我是5");
treeMap1.put(9, "我是9");
treeMap1.put(2, "我是2");
System.out.println(treeMap1.containsKey(5));
System.out.println(treeMap1.get(5));
System.out.println(treeMap1.firstKey() + ", 我最小");
System.out.println(treeMap1.lastKey() + ", 我最大");
System.out.println(treeMap1.floorKey(8) + ", 在表中所有<=8的数中,我离8最近");
System.out.println(treeMap1.ceilingKey(8) + ", 在表中所有>=8的数中,我离8最近");
treeMap1.remove(5);
System.out.println(treeMap1.get(5) + ", 删了就没有了哦");

哈希表能实现的,有序表都能实现,还能根据key之间有序 有新的功能,但它性能会差一些,O(logN)
在这里插入图片描述

2.链表

练习题:
1)反转单向双向链表

DoubleNode pre=null, next=null;
while(head!=null){
	next=head.next;
	head.next=pre;
	head.pre=next;
	pre=head;
	head=next;
}
return pre;

2)打印输出两链表公共部分
两链表从头开始比较,谁小谁移动,若相等,则打印并同时移动指针,有一个越界了停
在这里插入图片描述

3)
在这里插入图片描述
笔试碰到直接把链表value放入栈里面,然后依序比较链表值和栈弹出值完全一致 / 或单链表反转与原来对比
稍微省一些空间:只把右半边(包括中间值)放入栈里面,然后比较直到栈弹空【其实就是把右侧部分折过来】=> 快慢指针,快指针一次走两步,慢指针一次走一步:奇数个是慢指针先走,偶数个是快指针先走 {边界条件了解清楚练熟了,快指针走完了,慢指针在中点前,还是中点上,还是中点后}
而面试:
在这里插入图片描述

slow指针到中点时,中点指向null,同时右半部分指针反转,从两头开始新的指针遍历链表对比是否相同,如果到最后都一致或中间有不一致的情况,将链表还原并返回True或False。

public static boolean isPalindrome3(Node head) {
		if (head == null || head.next == null) {
			return true;
		}
		Node n1 = head;
		Node n2 = head;
		while (n2.next != null && n2.next.next != null) { // find mid node
			n1 = n1.next; // n1 -> mid
			n2 = n2.next.next; // n2 -> end
		}
		n2 = n1.next; // n2 -> right part first node
		n1.next = null; // mid.next -> null
		Node n3 = null;
		while (n2 != null) { // right part convert
			n3 = n2.next; // n3 -> save next node
			n2.next = n1; // next of right node convert
			n1 = n2; // n1 move
			n2 = n3; // n2 move
		}
		n3 = n1; // n3 -> save last node
		n2 = head;// n2 -> left first node
		boolean res = true;
		while (n1 != null && n2 != null) { // check palindrome
			if (n1.value != n2.value) {
				res = false;
				break;
			}
			n1 = n1.next; // left to mid
			n2 = n2.next; // right to mid
		}
		n1 = n3.next;
		n3.next = null;
		while (n1 != null) { // recover list
			n2 = n1.next;
			n1.next = n3;
			n3 = n1;
			n1 = n2;
		}
		return res;
	}

4)
在这里插入图片描述
笔试:单链表节点放Node数组,玩partition,再在数组里把值串起来。
面试:在这里插入图片描述
在这里插入图片描述
i所到之处把所有指针打乱,各部分头尾连起来,最后,小于等于大于三块区域重连的时候要讨论清楚(假如没有 等于区域 的边界条件)

public static Node listPartition2(Node head, int pivot) {
		Node sH = null; // small head
		Node sT = null; // small tail
		Node eH = null; // equal head
		Node eT = null; // equal tail
		Node bH = null; // big head
		Node bT = null; // big tail
		Node next = null; // save next node
		// every node distributed to three lists
		while (head != null) {
			next = head.next;
			head.next = null;
			if (head.value < pivot) {
				if (sH == null) {
					sH = head;
					sT = head;
				} else {
					sT.next = head;
					sT = head;
				}
			} else if (head.value == pivot) {
				if (eH == null) {
					eH = head;
					eT = head;
				} else {
					eT.next = head;
					eT = head;
				}
			} else {
				if (bH == null) {
					bH = head;
					bT = head;
				} else {
					bT.next = head;
					bT = head;
				}
			}
			head = next;
		}
		// small and equal reconnect
		if (sT != null) {
			sT.next = eH;
			eT = eT == null ? sT : eT;
		}
		// all reconnect
		if (eT != null) {
			eT.next = bH;
		}
		return sH != null ? sH : eH != null ? eH : bH;
	}

5)
在这里插入图片描述
由于无环,rand不可能指向自己,能用额外空间就哈希表,key Node类型 表老节点,Value Node类型 拷贝新节点,第一遍不考虑指针怎么连,就单纯拷贝值;第二遍在根据老链表Node的rand指针,next指针的get()查出它们的克隆指针(仅有value),并将新链表对应的Node的next和rand指针指向上述克隆指针位置。

public static Node copyListWithRand1(Node head) {
		HashMap<Node, Node> map = new HashMap<Node, Node>();
		Node cur = head;
		while (cur != null) {
			map.put(cur, new Node(cur.value));
			cur = cur.next;
		}
		cur = head;
		while (cur != null) {
			map.get(cur).next = map.get(cur.next);
			map.get(cur).rand = map.get(cur.rand);
			cur = cur.next;
		}
		return map.get(head);
	}
	
public static Node copyListWithRand2(Node head) {
		if (head == null) {
			return null;
		}
		Node cur = head;
		Node next = null;
		// copy node and link to every node
		while (cur != null) {
			next = cur.next;
			cur.next = new Node(cur.value);
			cur.next.next = next;
			cur = next;
		}
		cur = head;
		Node curCopy = null;
		// set copy node rand
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			curCopy.rand = cur.rand != null ? cur.rand.next : null;
			cur = next;
		}
		Node res = head.next;
		cur = head;
		// split
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			cur.next = next;
			curCopy.next = next != null ? next.next : null;
			cur = next;
		}
		return res;
	}

而不用哈希表:克隆节点就放在下一个,用它去串下一个;然后一对一对拿出来去处理(由于老链表的next指的就是3撇)
把哈希表省掉的原理:利用克隆节点的位置对应关系
在这里插入图片描述
6)
在这里插入图片描述
简单点还是用HashSet放每一个遍历的节点,每到一个节点就查一下Set.contains(Node),一旦查到就是第一个入环节点。
不用Set就是用快慢指针,首先明白一个问题,如果有环,在只有一个next指针的单链表情况下,那它一定会陷入里面出不来,也就是说,快指针走两步,慢指针走一步,快指针一旦走到空,一定无环;快指针一旦能追上慢指针(同时),一定有环,且不会在环里走两圈以上(速度是两倍,肯定能遍历完追上,所以是常数级)。
接下来就比较魔性,慢指针留在原点,快指针回到head,一定会在进入环的节点相遇(快指针此时腿打断了,只能一次走一步)
这是由于,假设无环长度为a,有环长度为b,此时慢指针走了x步,则快指针2x = x + nb,也就是在环里跑了n次才追上慢指针;即x = nb,又由于当慢指针跑到环入点的值一定是a + nb,所以慢指针再走a步一定会和快指针在环入点相遇。

public static Node getLoopNode(Node head) {
		if (head == null || head.next == null || head.next.next == null) {
			return null;
		}
		Node n1 = head.next; // n1 -> slow
		Node n2 = head.next.next; // n2 -> fast
		while (n1 != n2) {
			if (n2.next == null || n2.next.next == null) {
				return null;
			}
			n2 = n2.next.next;
			n1 = n1.next;
		}
		n2 = head; // n2 -> walk again from head
		while (n1 != n2) {
			n1 = n1.next;
			n2 = n2.next;
		}
		return n1;
	}

两个链表分别调用上述函数得到两个入环节点Loop1和Loop2。
① Loop1 = Null,Loop2=Null,如果相交只有可能是后续都共有;两个链表重新遍历,记录各自终点end1和end2,长度len1和len2:如果end1!=end2,则不相交,若相等,则长链表先走差值步(Max(len1,len2)-Min(len1,len2)),然后长短链表同时走,他们肯定会在相交点相遇。
在这里插入图片描述

public static Node noLoop(Node head1, Node head2) {
		if (head1 == null || head2 == null) {
			return null;
		}
		Node cur1 = head1;
		Node cur2 = head2;
		int n = 0; //用一个变量表长度差值:len1-len2
		while (cur1.next != null) { //到最后一个节点停,而不是到Null停
			n++;
			cur1 = cur1.next;
		}
		while (cur2.next != null) {
			n--;
			cur2 = cur2.next;
		}
		if (cur1 != cur2) {
			return null; //一定不相交
		}
		cur1 = n > 0 ? head1 : head2; // 谁长,谁的头变成cur1
		cur2 = cur1 == head1 ? head2 : head1; //谁短,谁的头变成cur2
		n = Math.abs(n);
		while (n != 0) {
			n--;
			cur1 = cur1.next;
		}
		while (cur1 != cur2) {
			cur1 = cur1.next;
			cur2 = cur2.next;
		}
		return cur1;
	}

② Loop1和Loop2 1个为null,另一个不为null : 不可能相交
③ Loop1和Loop2都有null,只会是下图三种情况
在这里插入图片描述
第二种最好求,loop1==loop2即可,然后计算head1和head2到入环点(当成end)的距离差值,其余和①一样
当loop1 !=loop2时,让loop1继续往下走,不能碰到loop2是情况一,返回Null,能碰到Loop2就是情况三,此时返回Loop1和Loop2都对,只不过就是离head1和head2距离远近而已。


public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
	Node cur1 = null;
	Node cur2 = null;
	if (loop1 == loop2) {
		cur1 = head1;
		cur2 = head2;
		int n = 0;
		while (cur1 != loop1) {
			n++;
			cur1 = cur1.next;
		}
		while (cur2 != loop2) {
			n--;
			cur2 = cur2.next;
		}
		cur1 = n > 0 ? head1 : head2;
		cur2 = cur1 == head1 ? head2 : head1;
		n = Math.abs(n);
		while (n != 0) {
			n--;
			cur1 = cur1.next;
		}
		while (cur1 != cur2) {
			cur1 = cur1.next;
			cur2 = cur2.next;
		}
		return cur1;
	} else {
		cur1 = loop1.next;
		while (cur1 != loop1) {
			if (cur1 == loop2) {
				return loop1;
			}
			cur1 = cur1.next;
		}
		return null;
	}
}
public static Node getIntersectNode(Node head1, Node head2) {
	if (head1 == null || head2 == null) {
		return null;
	}
	Node loop1 = getLoopNode(head1);
	Node loop2 = getLoopNode(head2);
	if (loop1 == null && loop2 == null) {
		return noLoop(head1, head2);
	}
	if (loop1 != null && loop2 != null) {
		return bothLoop(head1, loop1, head2, loop2);
	}
	return null;
}

五.二叉树

1.哈希表和有序表

二叉树递归时会进出三次本节点,此时顺序是递归序(self = null return; self.left; self.right;),在递归序的基础上延续出了先序中序后序遍历。
在这里插入图片描述
非递归
在这里插入图片描述
改一下第三步,先左后右,那么就是前序丿,打印出来就是 头右左;但如果不打印,再放到新的一个(收集)栈里面,打印出来就是左右头(后序遍历)

public static void posOrderUnRecur1(Node head) {
		System.out.print("pos-order: ");
		if (head != null) {
			Stack<Node> s1 = new Stack<Node>();
			Stack<Node> s2 = new Stack<Node>();
			s1.push(head);
			while (!s1.isEmpty()) {
				head = s1.pop();
				s2.push(head);
				if (head.left != null) {
					s1.push(head.left);
				}
				if (head.right != null) {
					s1.push(head.right);
				}
			}
			while (!s2.isEmpty()) {
				System.out.print(s2.pop().value + " ");
			}
		}
		System.out.println();
	}

中序非递归就是逮住一棵树,先把它左子树全压栈里面,(到无左子树时)然后依次弹出打印节点的过程中,对弹出节点的右树重复

public static void inOrderUnRecur(Node head) {
		System.out.print("in-order: ");
		if (head != null) {
			Stack<Node> stack = new Stack<Node>();
			while (!stack.isEmpty() || head != null) {
				if (head != null) { //head复用遍历左节点
					stack.push(head);
					head = head.left;
				} else {
					head = stack.pop(); //此时可能栈空
					System.out.print(head.value + " ");
					head = head.right; //但如果进了右树还有搞头
				}
			}
		}
		System.out.println();
	}

能这么写是因为,整棵树是可以被左树分解的,按这个左边界遍历进栈时先头再左,出栈自然就是先左后头;最后再在右子树上进行左边界遍历进栈的过程。
在这里插入图片描述

2.BFS宽度优先遍历(层次遍历)

思路:采用队列,头节点进去,然后打印,先放左再放右

扩展:获取树的最宽层长度和第几层:需要多一个表来记录是第几层

public static void BFS_GetMaxWidth(Node head){
        if (head == null)
            return;
        Queue<Node> queue = new LinkedList<>();
        queue.add(head);

        HashMap<Node, Integer> levelMap = new HashMap<>(); // 记录当前节点在哪一层
        levelMap.put(head,1);
        int curLevel = 1; //记录当前层
        int curLevelNode = 0; //记录当前层节点数
        int max = Integer.MIN_VALUE;

        while (!queue.isEmpty()){
            Node cur = queue.poll(); //弹出后才考虑相关操作以及左右孩子进队列
            int curNodeLevel = levelMap.get(cur); //确定当前节点在哪一层
//            System.out.println(cur.value);
            if (curNodeLevel == curLevel){// BFS时没进左右树
                curLevelNode++;
            }else {
                max = Math.max(max,curLevelNode); //不在前一层,可以开始同级了
                curLevel++;
                curLevelNode = 1; //这是发现的第一个子树节点
            }
            if (cur.left!=null){
                levelMap.put(cur.left,curNodeLevel+1); //进Map的时候确定好层数,在弹出的时候才知道现在是哪层
                queue.add(cur.left);
            }

            if (cur.right!=null){
                levelMap.put(cur.right,curNodeLevel+1);
                queue.add(cur.right);
            }

        }
    }

PS:看代码别硬看,它设置多少属性,就在纸上写几个变量,画图慢慢死扣还原。

不用哈希表:
在这里插入图片描述
队列头进尾出+四个变量:当前弹出层/下一层最后一个节点,当前层目前几个节点,max;
流程:1节点进队列,curend=①,nextend=null,curlevelNode=max=0;
1节点出队列,先让左孩子2进队列,先看nextend等不等于null,如果等于null,令nextend=②;右孩子3再进队列,nextend=③;
然后判断,当前层多发现一个节点,curlevelNode=1,当前节点是不是本层最后一个节点(1节点curend)=>max=curlevelNode;
接下来就是进入下一层,curend=nextend=③,nextend=null,curlevelNode=0,然后继续;
2出,4进,nextend=④,②!=curend=>curlevelNode++;
3出,56进,nextend=⑥,③ == curend=>max=++curlevelNode;
curend=nextend,nextend=null,curlevelNode=0;
4出,④!=curend=>curlevelNode++;
5出,7进,nextend=⑦,⑤!=curend=>curlevelNode++;
6出,8进,nextend=⑧,⑥
curend=>max=++curlevelNode =3;
7出,8出就不说了

nextend永远变成最新进队列的,当当前节点==curend时,本层结束,计算max,curend=nextend,其余初始化
也就是说利用curend和nextend完成滚动更新

3.套路题(可树型DP)

在这里插入图片描述
1)搜索二叉树时用中序,判断弹出节点value是否升序,不升序则false。

public static boolean checkBST(Node head){
        if(head == null){
            return true;
        }
        boolean isLeftBst = checkBST(head.left);
        if (!isLeftBst){
            return false;
        }
        if (preValue >= head.Value){ // 如果值不重复
            return false;
        }else{
            preValue = head.Value;   
        }
        return checkBST(head.right);    
    }

2)层次遍历
① if 如果碰到某个节点有右无左,返回false
② elseif 碰到某个节点有左但无右,后续节点必须都是叶子节点(无右无左)

public static boolean isCBT(Node head) {
		if (head == null) {
			return true;
		}
		LinkedList<Node> queue = new LinkedList<>();
		boolean leaf = false;
		Node l = null;
		Node r = null;
		queue.add(head);
		while (!queue.isEmpty()) {
			head = queue.poll();
			l = head.left;
			r = head.right;
			if ((leaf && (l != null || r != null))//如果遇到了不双全的节点后,又发现当前节点居然有孩子
				||
				(l == null && r != null)) {//有右孩子,但无左孩子
				return false;
			}
			if (l != null) {
				queue.add(l);
			}
			if (r != null) {
				queue.add(r);
			} else { //有左无右
				leaf = true;
			}
		}
		return true;
	}

4)平衡二叉树
套路:解决树型DP问题
先列出满足题目的self节点条件:满足,左右子树平衡,且|左-右|≤1
再罗列向左右树索要何种信息=>因此需要左树:平衡和高度;右树:平衡和高度

public static boolean isBalanced(Node head) {
		return process(head).isBalanced;
	}

	public static class ReturnType {
		public boolean isBalanced;
		public int height;

		public ReturnType(boolean isB, int hei) {
			isBalanced = isB;
			height = hei;
		}
	}

	public static ReturnType process(Node x) {
		if (x == null) {
			return new ReturnType(true, 0);
		}
		ReturnType leftData = process(x.left);
		ReturnType rightData = process(x.right);
		int height = Math.max(leftData.height, rightData.height);
		boolean isBalanced = leftData.isBalanced && rightData.isBalanced
				&& Math.abs(leftData.height - rightData.height) < 2;
		return new ReturnType(isBalanced, height);
	}

同理,搜索二叉树也可以
一是保证左右子树是搜索二叉树,且左max<x,右min>x
二是左:是否搜索二叉树,max;右:boolean,min
又由于递归每次返回一样的,因此需返回全集三个信息:搜,max,min

boolean isBST = false;
if(
	(leftData!=null ? (leftData.isBST && (leftData.max < x.value)) : true)
	&&
	(rightData!=null ? (rightData.isBST && (rightData.min > x.value)) : true)
){
	isBST = true;
}
if(rightData!=null)//左子树同理
	max = rightData.max
return new ReturnDate(isBST, min, max);

3)满二叉树
简单做法:分别统计二叉树最大深度L和节点个数N,如果满足2^L-1=N即可
借助套路(向左右树要信息):

public static boolean isFull(Node head){
        if (head == null){
            return  true;
        }
        info data = process4(head);
        return data.nodes == (1 << data.heigth-1);
    }

    public static class info{
        int heigth;
        int nodes;
        public info(int h, int n){
            heigth = h;
            nodes = n;
        }
    }
    public static info process4(Node head){
        if (head == null){
            return new info(0,0);
        }
        info leftInfo = process4(head.left);
        info rightInfo = process4(head.right);
        int height = Math.max(leftInfo.heigth,rightInfo.heigth)+1;
        int nodes = leftInfo.nodes+rightInfo.nodes+1;
        return new info(height,nodes);
    }

但套路不能全能,比如要整棵树的中位数,不能通过向左右子树要中位数得解。

4.给定两个二叉树的节点node1和node2,找到他们的最低公共祖先节点
简单方法:先用HashMap的key-value存遍历时的节点和其父节点。再用HashSet存node1开始通过fartherMap.get(cur)往上窜,直到头节点;之后再从node2开始往上窜,边检查在不在Set存的node1父亲链里,找第一个。
复杂方法:
分情况思考
1)o1是o2最低公共祖先 或 反过来
2)o1和o2不互为公共最低祖先,向上遍历才能找到
在这里插入图片描述

public static Node lowestAncestor(Node head, Node o1, Node o2) {
		if (head == null || head == o1 || head == o2) { //base case,有o返o
			return head; //返回null说明这颗子树既没有o1也没有o2
		}
		Node left = lowestAncestor(head.left, o1, o2);
		Node right = lowestAncestor(head.right, o1, o2);
		if (left != null && right != null) { //左右返回都不为空=>子树各有o1和o2=>从底一路往上扔
			return head;
		}
		return left != null ? left : right; //左右两棵树,不是都有返回值 => 会尽量返回不空的
	}

5.找一个节点的后继节点(中序遍历中 后一个节点)
这个简单就两个指针cur,net按中序遍历,net保存的是下一个位置的节点
在这里插入图片描述
但如果按上图所示的节点结构(有parent),想比之前O(N)更快的方式找到后继节点也是有可能的
也是两种情况,一是x有右树,那它的后继是右树的最左节点;(中序下一个:右边第一个最左)
二是x无右树,那其后继是依次parent想上找,看x是不是parent的左孩子,此时parent就是后继(因为对于parent来说,x是左树的最右节点);但还要考虑最后一个节点:也就是往上寻到head的时候,返回null。

 public static Node getSuccessorNode(Node node){
        if (node == null)
            return node;
        if (node.right!=null){
            node = node.right;
            while (node.left!=null)
                node = node.left; //一直往左
            return node;
        }else { 
            Node parent = node.parent;
            while (parent!=null && node!=parent.left){//情况二+最后节点的特殊情况
                node = parent;
                parent = node.parent;
            }
            return  parent;
        }
    }

6.二叉树序列化与反序列化:内存中的一棵树如何变成字符串形式,又如何从字符串形式变成内存里的树(1对1)。
如何判断一棵二叉树是不是另一棵二叉树的子树。
在这里插入图片描述

	public static String serialByPre(Node head) {//序列化
		if (head == null) {
			return "#!";
		}
		String res = head.value + "_";
		res += serialByPre(head.left);
		res += serialByPre(head.right);
		return res;
	}

	public static Node reconByPreString(String preStr) {//反序列化
		String[] values = preStr.split("_");
		Queue<String> queue = new LinkedList<String>();
		for (int i = 0; i != values.length; i++) {
			queue.offer(values[i]);
		}
		return reconPreOrder(queue);
	}

	public static Node reconPreOrder(Queue<String> queue) {
		String value = queue.poll();
		if (value.equals("#")) {
			return null;
		}
		Node head = new Node(Integer.valueOf(value));
		head.left = reconPreOrder(queue);
		head.right = reconPreOrder(queue);
		return head;
	}

折纸条
1次对折         1个凹折痕
2次对折          凹凹凸
3次折    凹凹凸    凹    凹凸凸
4次折 凹凹凸 凹 凹凸凸 凹 凹凹凸 凸 凹凸凸
每次新折痕都是在上次出现折痕上下两侧出现,上凹下凸
在这里插入图片描述
这是一颗头节点为凹折痕,左树凹,右树凸的满二叉树。
这题是中序,在第二次经历递归序的时候打印。

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);
    }

六.图

1.定义好通用的图所需结构
public class Edge {
	public int weight;
	public Node from; //出边
	public Node to; //入边

	public Edge(int weight, Node from, Node to) {
		this.weight = weight;
		this.from = from;
		this.to = to;
	}

}
public 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;
		in = 0;
		out = 0;
		nexts = new ArrayList<>();
		edges = new ArrayList<>();
	}
}
public class Graph {
	public HashMap<Integer,Node> nodes;
	public HashSet<Edge> edges;

	public Graph() {
		nodes = new HashMap<>();
		edges = new HashSet<>();
	}
}

下列代码就是将特殊类型的矩阵图表示转换为,我们经常用的图结构(上面三块):这里的矩阵表示每一行表示一个节点,第一个值为边的权重,第2,3个值为from和to节点。

public class GraphGenerator {

	public static Graph createGraph(Integer[][] matrix) {
		Graph graph = new Graph();
		for (int i = 0; i < matrix.length; i++) {
			Integer weight = matrix[i][0];
			Integer from = matrix[i][1];
			Integer to = matrix[i][2];
			if (!graph.nodes.containsKey(from)) {
				graph.nodes.put(from, new Node(from));
			}
			if (!graph.nodes.containsKey(to)) {
				graph.nodes.put(to, new Node(to));
			}
			Node fromNode = graph.nodes.get(from); //从一开始的Integer类型 转为 Node类型
			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;
	}

}
2.BFS和DFS + 拓扑排序
public static void bfs(Node node) {
		if (node == null) {
			return;
		}
		Queue<Node> queue = new LinkedList<>(); //宽度优先用队列
		HashSet<Node> map = new HashSet<>(); //为了解决无向图和有向图 中环结构跑到重复点问题
		queue.add(node);
		map.add(node);
		while (!queue.isEmpty()) {
			Node cur = queue.poll();
			System.out.println(cur.value); //具体操作
			for (Node next : cur.nexts) {
				if (!map.contains(next)) { 
					map.add(next);
					queue.add(next);
				}
			}
		}
	}
public static void dfs(Node node) {
		if (node == null) {
			return;
		}
		Stack<Node> stack = new Stack<>(); //深度优先选择栈
		HashSet<Node> set = new HashSet<>();
		stack.add(node);
		set.add(node);
		System.out.println(node.value); //一进来就处理
		while (!stack.isEmpty()) {
			Node cur = stack.pop(); //当前点弹出
			for (Node next : cur.nexts) {
				if (!set.contains(next)) {
					stack.push(cur); // 当前点再压回去
					stack.push(next); //再压入next=》下次保证弹出路径
					set.add(next);
					System.out.println(next.value); //不再看其他next了
					break;
				}
			}
		}
	}

B弹出,再把B压入,C压入(栈永远保持深度的路径)
在这里插入图片描述

// directed graph and no loop 也可以用于检测有无环
	public static List<Node> sortedTopology(Graph graph) { //入度为0点,然后删掉它的影响,再找入度为0点,依次
		// key:某一个Node,value:剩余的入度
		HashMap<Node, Integer> inMap = new HashMap<>();
		//入度为0的点才能进这个队列
		Queue<Node> zeroInQueue = new LinkedList<>();
		for (Node node : graph.nodes.values()) {
			inMap.put(node, node.in);
			if (node.in == 0) {
				zeroInQueue.add(node);
			}
		}
		//拓扑排序的结果,依次加入result
		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;
3.kruskal算法(边)和Prim算法(点)【无向图】

在这里插入图片描述
边从小到大考虑,如果加上不会形成环,就加上(会存在两片独立边集 突然连起来的情况)
具体做法是 先将每个点 各自看成一个独立集,由小边将这些集合一个个并起来,比如上图A和C就是通过权值最小边2 并起来的。

public static class MySets{
		public HashMap<Node, List<Node>> setMap;
		public MySets(Collection<Node> nodes){ //简单类并查集的实现,但没实际上并查集快
			for (Node cur : nodes){
				List<Node> set = new ArrayList<Node>();
				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);
			for (Node toNode : toSet){ //这有个for,不够快
				fromSet.add(toNode);
				setMap.put(toNode,fromSet);
			}
		}
	}
	
	public static Set<Edge> kruskal(Graph graph){
		MySets myset = new MySets(graph.nodes.values());
		PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator()); //o1-o2,负数返回第一个,即从小到大排序
		for (Edge edge : graph.edges){
			priorityQueue.add(edge);
		}
		Set<Edge> result = new HashSet<Edge>();
		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是以任意点出发,例如从Node A开始考虑它的所有Edge,选最小Edge连接新Node 同时解锁新Edge,再从所有Edge选最小
在这里插入图片描述

public static class EdgeComparator implements Comparator<Edge> {
		@Override
		public int compare(Edge o1, Edge o2) {
			return o1.weight - o2.weight;
		}
	}

	public static Set<Edge> primMST(Graph graph) {
		//解锁的边进入小根堆
		PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(
				new EdgeComparator());
		HashSet<Node> set = new HashSet<>(); //考察过的Node进Set,没出现就是新Node
		Set<Edge> result = new HashSet<>(); //依次挑选的边放在result
		for (Node node : graph.nodes.values()) { //随便挑了一个点=》由于整张图可能会不是连通图,一个点跑不完,只能各自最小生成树
			if (!set.contains(node)) { // A Node 开始
				set.add(node);
				for (Edge edge : node.edges) { //由一个点,解锁所有相连的边
					priorityQueue.add(edge);
				}
				while (!priorityQueue.isEmpty()) {
					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); // 虽然会把重复边放进去,但前面的if会判断点重不重复,因此即便重复也会跳过=》也可以用一个Edge Set优化常数时间
						}
					}
				}
			}
		}
		return result;
	}
4.Dijkstra:单元最短路径

从A出发 新解锁路径有没有比现有路径更小 有就更新,新解锁的Node再比较完所有路径后会锁死{ 没有权值为负数 的边/没有累加和负数的环 => 累加和无穷小}
在这里插入图片描述

public static HashMap<Node, Integer> dijkstra1(Node head) {
		// 从head出发到所有点的最小距离
		// key :  从head出发到达key (某一个节点)
		//value: 从head出发到达key 的最小距离
		// 如果在表中,没有T的记录,含义是从head出发到T这个点的距离为正无穷
		HashMap<Node, Integer> distanceMap = new HashMap<>(); // 假如 2 Node , 距离 7,意思是目前为止发现到2Node的最小距离为7
		distanceMap.put(head, 0); //到自己距离为0
		//以及求过距离的点,给锁起来放进selectedNodes中,以后再也不碰
		HashSet<Node> selectedNodes = new HashSet<>();

		Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);//在distanceMap中找到一个 非selectedNodes 的最小记录来处理它
		while (minNode != null) {
			int distance = distanceMap.get(minNode);
			for (Edge edge : minNode.edges) {
				Node toNode = edge.to;
				if (!distanceMap.containsKey(toNode)) { //表里没有=》正无穷,总是可以更新它
					distanceMap.put(toNode, distance + edge.weight); //一开始就 0+weight
				}
				distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight)); //修改最小distance
			}
			selectedNodes.add(minNode);
			minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
		}
		return distanceMap;
	}

	public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, 
			HashSet<Node> touchedNodes) { //每次通过遍历找最小
		Node minNode = null;
		int minDistance = Integer.MAX_VALUE;
		for (Entry<Node, Integer> entry : distanceMap.entrySet()) { //entry迭代
			Node node = entry.getKey();
			int distance = entry.getValue();
			if (!touchedNodes.contains(node) && distance < minDistance) { //找未touch的最小distance
				minNode = node;
				minDistance = distance;
			}
		}
		return minNode;
	}

当然迪杰斯特拉算法可以在寻找最小未选择点的时候加速:具体做法如下
维护一个小根堆,将所有未选择点放入,每次选择 堆顶并弹出,然后堆重新调整,并根据新弹出的节点,会更新堆中的值甚至增加新节点。
此时,系统提供的堆不再适用与其上操作的修改值后的heapify,不能改具体的某个值,只能给一个然后出来一个再调整成堆。只能自己写!

//改进后的dijkstra算法
//从head出发,所有head能到达的节点,生成到达每个节点的最小路径记录并返回
public static HashMap<Node, Integer> dijkstra2(Node head, int size){
    NodeHeap nodeHeap = new NodeHeap(size); //自定义一个堆,size大小
    nodeHeap.addOrUpdateOrIgnore(head, 0); //第一次出现就add;塞的记录比之前更小就update;新纪录 >= 原distance就ignore
    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.edge){  
            nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
        }
        result.put(cur, distance); //再放进result之前,把它相连的每个节点在小根堆上都更新一下
    }
}

public static class NodeRecord{
    public Node node;
    public int distance;

    public NodeRecord(Node node, int distance){
        this.node = node;
        this.distance = distance;
    }
}

七.详解前缀树和贪心算法

1.前缀树

在这里插入图片描述

经典前缀树字符一定是放在路上的。没有这条路就新建,有这条路就复用,当然实际上会在节点和路径上多一些别的信息。

在这里插入图片描述

public static class TrieNode {
		public int path; //在加前缀树的时候,这个节点到达(通过)多少次
		public int end; //这个节点是否是一个字符串的结尾节点,如果是的话,那它是多少字符串的结尾节点
		public TrieNode[] nexts; //有多少条路 // TreeMap/HashMap<Char, Node> nexts; //字符多(数组费空间)可以换成哈希表,key表示有某条路,value代表通过这条路走到下个节点上去

		public TrieNode() {
			path = 0;
			end = 0;
			// nexts[0] == null 没有走向'a'的路
			// nexts[0] != null 有走向'a'的路
			// ......
			// nexts[25] != null 有走向'z'的路
			nexts = new TrieNode[26]; //每个Node下面都可能会有26条路(26个字母),只是走没走/建没建而已
		}
	}

在这里插入图片描述

public static class Trie {
		private TrieNode root;

		public Trie() {
			root = new TrieNode();
		}

		public void insert(String word) { //加入一个字符串
			if (word == null) {
				return;
			}
			char[] chs = word.toCharArray();
			TrieNode node = root;
			node.path++; //一开始根节点p++:加入多少个字符串;而其e表示有多少个空串
			int index = 0;
			for (int i = 0; i < chs.length; i++) { //从左往右遍历字符串
				index = chs[i] - 'a'; //由字符,对应成走向哪条路
				if (node.nexts[index] == null) {
					node.nexts[index] = new TrieNode(); //没有就新建
				}
				node = node.nexts[index]; //有就下一位
				node.path++;
			}
			node.end++; //到结尾
		}

		public void delete(String word) {
			if (search(word) != 0) { //确认树中加入过word,才删除
				char[] chs = word.toCharArray();
				TrieNode node = root;
				int index = 0;
				for (int i = 0; i < chs.length; i++) {
					index = chs[i] - 'a';
					if (--node.nexts[index].path == 0) { //如果删没了==自己包括后面都没路了,这个节点标空
						node.nexts[index] = null;
						return;
					}
					node = node.nexts[index];
				}
				node.end--;
			}
		}

		public int search(String word) {
			if (word == null) {
				return 0;
			}
			char[] chs = word.toCharArray();
			TrieNode node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.nexts[index] == null) { //查到一半发现没路了:即不存在
					return 0;
				}
				node = node.nexts[index];
			}
			return node.end; //查prefix的话 return node.path;
		}
	}
2.贪心算法

(面试很少出现,01结论 区分度不高)

在这里插入图片描述
在这里插入图片描述

题7:FIFS,SJF都不行,还是以谁先结束谁先就局部最优(保证剩下的时间最多),和已经安排到的会议有重合的会议就跳过

public static int bestArrange(Program[] programs, int timePoint) {//timePoint即会议分配的时间
		Arrays.sort(programs, new ProgramComparator()); //用一个比较器就行
		int result = 0;
		for (int i = 0; i < programs.length; i++) { //从左往右依次遍历所有会议
			if (timePoint <= programs[i].start) { //timePoint比现在最小的start是不是要小(也就是该会议能不能安排得上)
				result++;
				timePoint = programs[i].end; //这里start和end表示各自会议的开始和结束时间点
			}
		}
		return result;
	}

在这里插入图片描述

3.字典序证明

字典序证明:一个数组中所有字符串拼接起来,使得最终形成的字符串字典序最小
长度一样比较:每位当成26进制数来比较
长度不一:短的右边补0,如”b“和”ba“=>“b0”<“ba”
直观想法是按照每个字符串各自的字典序来排:但"bba">“bab”
在这里插入图片描述
于是升级成比较两位的

public static class MyComparator implements Comparator<String> {
		@Override
		public int compare(String a, String b) {
			return (a + b).compareTo(b + a);
		}
	}

	public static String lowestString(String[] strs) {
		if (strs == null || strs.length == 0) {
			return "";
		}
		Arrays.sort(strs, new MyComparator());
		String res = "";
		for (int i = 0; i < strs.length; i++) {
			res += strs[i];
		}
		return res;
	}

但碰到无传递性的比较策略就麻瓜了,比如斗兽棋,鼠本来最小,但它却能吃掉最大的大象,这就成比较无效(原始数据值一样但顺序不一样得到的结果也不一样)

证明代码有效如下:
比如,a.b ≤ b.a + b.c ≤ c.b => a.c ≤ c.a(传递性)
str -> k进制数
比如"abc"拼接"de" => “abc” * k^2 + “de”
a.b <=> a * k^b.length + b == a * m(b) + b 【m(b)就是改写成一个函数好理解】
在这里插入图片描述
这两张图证明比较策略具有传递性:可以排出唯一序列,且前 . 后 ≤ 后 . 前
在这里插入图片描述
因此在【。。。a。。。b。。。】的初始字典序中交换a和b后想证明其比交换前小
需要通过相隔字符的传递性【下图有点像线性代数的逆序数】
在这里插入图片描述

后续还需通过数学归纳法推得n=3,以及n=n时有效
所以,证明还是算了,不如根据某标准建立一个比较器来排序或组成堆的贪心算法+对数器(全排列暴力+数组随机生成)靠谱

4.其他题目

在这里插入图片描述
思路:把数组所有数放到小根堆里面去,每次弹出最小的两个,然后把合起来的数再放入小根堆,再拿出两个做结合,依次弹出至堆只剩放回的那一个。{哈夫曼编码}

public static int lessMoney(int[] arr) {
		PriorityQueue<Integer> pQ = new PriorityQueue<>();
		for (int i = 0; i < arr.length; i++) {
			pQ.add(arr[i]);
		}
		int sum = 0;
		int cur = 0;
		while (pQ.size() > 1) {
			cur = pQ.poll() + pQ.poll(); //每次弹两个,结合一个
			sum += cur; //算累加和
			pQ.add(cur); // 重新仍入小根堆
		}
		return sum;
	}

在这里插入图片描述
思路:一开始把所有项目加入小根堆(按花费)里去,这是被锁住的项目,然后再依次根据是否<m弹出到大根堆(按利润),即解锁的项目:总结就是局部:花费最小,利润最大。最后就是弹出大根堆的堆顶,并修改m,继续上述行为【串行做有点像银行家算法】

public static int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) { //W即m
		Node[] nodes = new Node[Profits.length];
		for (int i = 0; i < Profits.length; i++) {
			nodes[i] = new Node(Profits[i], Capital[i]); //Capital即cost
		}

		PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator()); //o1-o2
		PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator()); //o2-o1
		for (int i = 0; i < nodes.length; i++) { //所有项目扔进被锁的 由花费组织的小根堆
			minCostQ.add(nodes[i]);
		}
		for (int i = 0; i < k; i++) { //进行k轮
			while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) { //能力所及项目全解锁
				maxProfitQ.add(minCostQ.poll());
			}
			if (maxProfitQ.isEmpty()) { //剩下项目都做不了了
				return W;
			}
			W += maxProfitQ.poll().p; //完成一项就+利润
		}
		return W;
	}

在这里插入图片描述

两个堆,最后如果是偶数个,弹出两个堆顶取平均,奇数个 弹出size大的那个的堆顶即可。在这里插入图片描述

八.暴力递归

在这里插入图片描述
就是把大问题分解成小问题进行决策,一直分划到从一个范围变成一个值的base case,这个过程中不记录子问题的解(就纯分)。

1.N皇后

N皇后:其实行和列一眼就能验证,但主要要把斜线要描述的好。

在这里插入图片描述
回溯:暴力递归 这个n!

public static int num1(int n) {
		if (n < 1) {
			return 0;
		}
		int[] record = new int[n]; //record[i]表示第i行的皇后摆在第value列 record[0]摆好在 record[1] record[2]
		return process1(0, record, n);
	}
	// 潜台词:record[0...i-1]的任意两个皇后不共行,不共列,不共斜线
	// 返回之是摆完所有皇后,合理摆法多少种
	public static int process1(int i, int[] record, int n) { //目前来到第i行。record[0...i-1]表示之前行所放皇后位置。n表示整体有多少行
		if (i == n) { //终止行,0-n-1这n个摆好了,base case达成,成功result+1
			return 1;
		}
		int res = 0;
		for (int j = 0; j < n; j++) {
			//当前i行皇后放在j列,会不会和之前(0...i-1)的皇后共行共列共斜线,是则无效,不是则有效
			if (isValid(record, i, j)) {
				record[i] = j;
				res += process1(i + 1, record, n);
			}
		}
		return res;
	}
	//record[0...i-1]你需要看,record[i...]不需要看
	//返回i行皇后,放在了j列,是否有效
	public static boolean isValid(int[] record, int i, int j) {
		for (int k = 0; k < i; k++) { //之前某行的皇后
			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(k - i)) {//不共列+ 行之差绝对值!=列之差绝对值
				return false;
			}
		}
		return true;
	}

时间复杂度是n!,除非按论文方法,否则指标上已是最优,唯一优化就是在常数级通过位运算对列行斜线限制代理record做检查。
在这里插入图片描述

2.暴力递归冲冲冲

①汉诺塔问题
其实就是分成三个圆盘 from、to和other,和一个移动函数P(i, from, to, other)

  1. 1 - i-1个圆盘从from->other上去 ——》P(i-1, from, other, to)
  2. 第 i 个圆盘从from->to上 ——》 打印 Print
  3. 1 - i-1个圆盘从other->to上去 ——》P(i-1, other, to, from)

尝试的时候只需要保证在这个局部下拆解是对的,那他就是对的

public static int func(int n, 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); //i不违规,i-1一样也不违规
        System.out.println("Move "+i+ " from "+ start + " to " + end);
        func(i-1, other, end, start);
    }
}

② 打印一个字符串全部子序列,包括空串。
思路:从左往右每一个字符开始,每个字符都有 要或者不要 这两条路,相当于从一开始就是做一个二叉树的子集筛选。
其中可以把之前的结果存在List中,或者省内存的话,可以通过tmp暂存str[i],str[i] = 0; ascii对应“\0”不打印,最后str[i]=tmp

public static void process1(char[] str, int i){
    if(i == str.length){
        System.out.println(String.valueOf(str));
    }
    process(str, i+1); //保留第i个字符
    char tmp = str[i]; //保留在系统栈里
    str[i] = 0; //相当于删除该字符,相当于分配ASCII字符“\0”,也称为空字符或字符串终止符。这个字符不会在屏幕上打印任何内容,但它占用了一个字节的内存
    process(str, i+1); //不保留
    str[i] = tmp; //又还回来,实现空间复用
}

③ 打印一个字符串全部排列,要求不重复
思路:第一个位置n种可能性,第二个位置n-1种,…

//str[i...]范围上,所有的字符,都可以在i位置上,后续去尝试
//str[0...i-1]范围上,是之前做的选择
//res存放之前所有字符串形成的全排列
public static void process2(char[] str, int i, ArrayList<String> res){
    if(i==1){
        res.add(String.valueOf(str));
    }
    boolean[] visit = new boolean[26]; //visit[0 1 ... 25]表示试没试过,这里是默认全是小写字母,表示当前节点对26个字母使用情况
    for(int j=i ; j < str.length; j++){
        if(!visit[str[j] - 'a']){ //不是之前走所有路取去重结果,这里是直接限定分支,常数快一些 > 剪枝,分支限界
            visit[str[j] - 'a'] = true;
            swap(str, i, j); //后续j和i互换位置,i==j代表不交换的排列
            process2(str, i+1, res);
            swap(str, i, j); //回溯,回到进入递归之前的状态
        }   
    }
}


在这里插入图片描述
这里的绝顶聪明其实就是获取自己先手情况下的最大分数,且让对方获得该情况下的最低分数,但是此最低分数是相对的,不代表比先手的最佳分数低。

public static int win(int[] arr){
    if(arr == null || arr.length == 0){
        return 0;
    }
    return Math.max(first(arr,0,arr.length-1), second(arr,0,arr.length-1));
}
public static int first(int[] arr, int l, int r){//先手
    if(l==r){
        return arr[l]; //先手只有唯一选择
    }
    return Math.max(arr[l]+second(arr, l+1, r), arr[r]+second(arr, l, r-1));//A这么聪明,当然是最大化(抽左边+后手抽其他,抽右边+后手抽其他)
}

public static int second(int[] arr, int l, int r){//后手
    if(l==r){
        return 0; //后手没得拿
    }
    return Math.min(first(arr, l+1, r), first(arr, l, r-1));//后手只能被决定,对方会从拿走l和r中选择对我而言最小的
}

⑤ 逆序栈,且不申请额外的数据结果,只能递归
思路:这就需要一个函数,首先能做到移除栈内元素,再返回栈底元素,其余栈内元素放回
在这里插入图片描述

public static int tmpsave(Stack<Integer> stack){
    int result = stack.pop(); //递归暂存站内元素
    if(stack.isEmpty){
        return result; //返回栈底元素 
    }else{
        int last = tmpsave(stack); //向下
        stack.push(result); //存回站内元素
        return last;
    }
}

在这里插入图片描述
这里其实就再延续之前style,取栈底,递归调,到栈空,往回压。

public static void reverse(Stack<Integer> stack){
    if(stack.isEmpty()){
        return;
    }
    int i = tmpsave(stack);
    reverse(stack);
    stack.push(i);
}

⑥ 规定1和A对应,2和B对应…,这样数字111可以转化为“AAA”,“KA”和“AK”,
给定一个只有数字字符组成的字符串str,返回有多少种转化结果。
思路:仍然是从左往右试,不能更改的[0-i-1 ] + i 位置开始试 + 递归后面 [i+1 …]
base case: i 位置是0字符,由于前面i-1均确定,0字符没法单独做决定,只能返回0。
i 位置不是0,则
1)i 位置单独直接转化为对应字母,+ [i+1]之后数字的转化
2)i = 1 || i = 2时,既可以走情况一,也可以和 i + 1位置一起转化为对应字母(不超过26)

//i之前位置如何转化已经决定好了,i之后的位置有多少种转化结果
public static int transfer(char[] str, int i){
    if(i==str.length){
        return 1; //到最后了,之前做的决定有效,1种可行结果
    }
    if(str[i]=='0'){
        return 0; //之前做的决定有效  但在看到这个位置发现没法继续转化了,只能无效,返回0种
    }
    if(str[i]=="1"){
        int res = transfer(str, i+1); //i自己作为单独部分,后续有多少种方法
        if( i+1 < str.length){
            res += transfer(str, i + 2); //(i和i+1)作为单独部分,后续多少种
        }
        return res;
    }
    if(str[i]=="2"){
        int res = transfer(str, i+1); 
        if( i+1 < str.length && str[i+1]>='0' && str[i+1]<='6'){//小于26
            res += transfer(str, i + 2); 
        }
        return res;
    }
    return transfer(str, i+1); //'3'-'9'
}

⑦ 01背包问题: 给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?
思路:从左到右试呗,0号要或不要,1号要或不要…

//i之后的货物自由选择,形成最大价值返回(不超过bag)
public static int process1(int weights[], int[] values, int i, int alreadWeight, int bag){
    if(alreadWeight > bag){ //之前做的决定所达到的重量 alreadWeight
        return -values[i-1]; //加第i-1个的时候超重了,得减去那个超重的物品
    }
    if(i == weights.length){
        return 0;
    }
    return Math.max(process1(weights,values,i+1,alreadWeight,bag),  //不加该物品
    values[i] + process1(weights,values,i+1,alreadWeight+weights[i],bag)); //加[i]物品
}

public static int process2(int weights[], int[] values, int i, int alreadWeight, int alreadValue, int bag){
    if(alreadWeight > bag){
        return 0; 
    }
    if(i == values.length){
        return alreadValue; //alreadValue表示已加背包的物品价值
    }
    return Math.max(process2(weights,values,i+1,alreadWeight,alreadValue,bag),  
     process2(weights,values,i+1, alreadWeight+weights[i], alreadValue+values[i], bag)); 
}

找可变参数少,形式简单的(值就可以那就不用list),容易改动态规划 dp是根据试法不同搞得东西

Ending

接下来就是LeetCode实战Practice了,后续也可跟着左神的基础提升班继续啊

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值