部分完整代码地址
附:排版有点乱,后续有时间的话,我会重新修改一下markdown文件,感谢阅读~
题库
时间复杂度
认识时间复杂度
常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的指标。常用O(读作big O)来表示。具体来说,在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分,如果记为f(N),那么时间复杂度为O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。
例1: 一个简单的理解时间复杂度的例子:一个有序数组A,另一个无序数组B,请打印B中的所有不在A中的数,A数组长度为N,B数组长度为M。
算法流程1:对于数组B中的每一个数,都在A中通过遍历的方式找一下;
算法流程2:对于数组B中的每一个数,都在A中通过二分的方式找一下;
算法流程3:先把数组B排序,然后用类似外排的方式打印所有在A中出现的数;
三个流程,三种时间复杂度的表达... 如何分析好坏?
对数器
对数器的概念和使用
0,有一个你想要测的方法a, 1,实现一个绝对正确但是复杂度不好的方法b, 2,实现一个随机样本产生器 3,实现比对的方法 4,把方法a和方法b比对很多次来验证方法a是否正确。 5,如果有一个样本使得比对出错,打印样本分析是哪个方法出错 6,当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
代码实现
// 生出长度随机的数组 // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // 比较两个数组 public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } //一个绝对正确的方法 public static void comparator(int[] arr) { Arrays.sort(arr); } //拷贝数组 public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // 对数器 public static void main(String[] args) { int testTime = 500000; //数组长度 int maxSize = 100; //数在0-100之间 int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); bubbleSort(arr1); comparator(arr2); if (!isEqual(arr1, arr2)) { succeed = false; break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); int[] arr = generateRandomArray(maxSize, maxValue); printArray(arr); bubbleSort(arr); printArray(arr); }
排序
冒泡排序
冒泡排序细节的讲解与复杂度分析
时间复杂度O(N^2),额外空间复杂度O(1)
思路:每次找最大的数放在后面,范围不断缩小
代码:
// 时间复杂度O(N^2),额外空间复杂度O(1)
public class BubbleSort {
public static void bubblesort(int [] arr){
for(int i = arr.length - 1;i > 0 ;i --){
for(int j = 0; j < i;j++){
if(arr[j] > arr[j+1]){
swap(arr, j, j+1);
}
}
}
}
public static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String [] args){
int[] arr = {4 , 2 , 3 , 1};
bubblesort(arr);
System.out.println(Arrays.toString(arr));
}
}
选择排序
选择排序的细节讲解与复杂度分析
时间复杂度O(N^2),额外空间复杂度O(1)
思路:0到n-1上最小的数和0位置上交换,1到n-1上最小的数和1位置上交换,以此类推
代码:
// 时间复杂度O(N^2)额外空间复杂度O(1)
public class SelectionSort {
public static void selectsort(int[] arr){
for(int i = 0; i < arr.length; i++){
int minIndex = i;
for(int j = i + 1; j
插入排序
插入排序的细节讲解与复杂度分析
时间复杂度O(N^2),额外空间复杂度O(1)
思路:就像自己手里攥了好多牌,攥的牌是已经排好的,但是新抓了一张牌,这张牌就看能滑到哪个位置上插进去
代码:
// 时间复杂度O(N^2),额外空间复杂度O(1)
public class InsertSort {
public static void insertsort(int[] arr){
for(int i = 1; i < arr.length; i++){
for(int j = i - 1;j >= 0 && arr[j+1] < arr[j]; j--){
swap(arr, j, j+1);
}
}
}
public static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String [] args){
int[] arr = {5, 3, 2, 4, 1};
insertsort(arr);
System.out.println(Arrays.toString(arr));
}
}
归并排序
master公式
剖析递归行为和递归行为时间复杂度的估算
一个递归行为的例子
master公式的使用
T(N) = a*T(N/b) + O(N^d)
1) log(b,a) > d -> 复杂度为O(N^log(b,a))
2) log(b,a) = d -> 复杂度为O(N^d * logN)
3) log(b,a) < d -> 复杂度为O(N^d)
补充阅读:www.gocalf.com/blog/algorithm-complexity-and-mastertheorem.html
master适用范围:所有子过程规模必须一样
归并排序
归并排序的细节讲解与复杂度分析
时间复杂度O(N*logN),额外空间复杂度O(N)
归并排序快的实质:没有浪费比较,充分利用了每次比较,因为冒泡排序等排序方法,每次比较只搞定了一个数,浪费了很多比较
代码:
public class Merge {
public static void mergesort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
mergesort(arr, 0, arr.length-1);
}
public static void mergesort(int[] arr, int l, int r){
//递归函数出口
if(l == r){
return;
}
int mid = l + ((r - l) >> 1);
//左边排好序,右边排好序
mergesort(arr, l, mid);
mergesort(arr, mid + 1, r);
//利用外排将左右排好序的数组放在一个新的数组中,然后拷贝给原数组
merge(arr, l, r, mid);
}
public static void merge(int arr[], int l, int r, int m){
int i = 0;
int p1 = l;
int p2 = m+1;
//生成辅助数组
int[] help = new int[r - l + 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){
help[i++] = arr[p2++];
}
for(int j= 0; j < help.length; j ++){
arr[l + j] = help[j];
}
}
public static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String [] args){
int[] arr = {4 , 2 , 1, 3};
mergesort(arr);
System.out.println(Arrays.toString(arr));
}
}
小和问题和逆序对问题
小和问题:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子: [1,3,4,2,5] 1左边比1小的数,没有; 3左边比3小的数,1; 4左边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、2; 所以小和为1+1+3+1+1+3+4+2=16 思路: 我在每次merge过程中,先将产生的小和存起来,然后将当前部分进行排序,再后续炸出小和 先边排序边求左部分小和,然后边排序边求右部分小和,然后整体边排序边求小和 排序和求小和过程:谁小谁后移动,若是左部分小,res += (右面当前位置往后元素的个数) * 左部分当前位的值,若是右半部分小,不产生小和,不做计算,指针后移 代码: public class SmallSum { public static void mergesort(int[] arr){ if (arr == null|| arr.length < 2) { return; } System.out.println(mergesort(arr, 0, arr.length - 1)); } public static int mergesort(int[] arr, int l, int r){ if(l == r){ return 0; } /*不使用(l + r)/2,而是使用l + ((r - l)>>1),可以防止溢出,而且位运算比算术运算快很多 注意右移必须加括号,要不会报错 */ 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 p1 = l; int p2 = m + 1; int res = 0; int i= 0; /*这里的长度也要根据传进来的数判断,不能生成随便长度的数组*/ int[] help = new int[r - l + 1]; while(p1 <= m && p2 <= r){ res += arr[p2] > arr[p1] ? arr[p1] * ( r - p2 + 1 ) : 0; help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; } while(p1<=m){ help[i++] = arr[p1++]; } while(p2 <= r){ help[i++] = arr[p2++]; } /*这里注意是help.length和arr[l+j],不能写j,万一参数是2或者3什么的,这里的差别就很明显了,因为每次传进来的参数不一样,长度就有可能不一样,*/ for(int j =0;j < help.length;j++){ arr[l+j] = help[j]; } return res; } public static void main(String [] args){ int[] arr = {2, 3, 1, 4}; mergesort(arr); } }
逆序对问题
在一个数组中,左边的数如果比右边的数大,则折两个数构成一个逆序对,请打印所有逆序对。
快排
-
例题1
给定一个数组arr,和一个数num,请把小于等于num的数放在数 组的左边,大于num的数放在数组的右边。 要求额外空间复杂度O(1),时间复杂度O(N)
例题2:(荷兰国旗问题)
问题:给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。 要求:额外空间复杂度O(1),时间复杂度O(N) 思路: 三个变量 less: 存储小于区域的最后一个位置 more:存储大于区域的第一个位置 cur:当前位置 当 当前位置的数小于num,那么就将cur指向的数和小于区域下一个位置交换,然后less ++(小于区域扩大一个位置), cur++(指针向后移动一位) 当 当前位置的数等于num,什么都不做,cur++(cur向后移动一位),继续判断, 当 当前位置的数大于num,将cur指向的数和大于区域的前一个位置交换,然后more --,然后cur不变,继续判断我换过来的这个数,大于小于还是等于num 代码: public class NetherlandsFlag { public static void partition(int[] arr, int num ,int L, int R){ int less = L -1; int more = R + 1; int cur = L; while(cur < more){ if(arr[cur] < num){ swap(arr, cur++, ++less); } else if(arr[cur] > num){ swap(arr, cur, --more); } else{ cur ++; } } } public static void swap(int[] arr, int i,int j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main(String [] args){ int num = 5; int[] arr = {10, 9, 8, 5, 7, 6, 5, 4, 3, 2, 1}; partition(arr, num, 0, 10); System.out.println(Arrays.toString(arr)); } }
经典快排
思路:就是在荷兰国旗的问题上,将大于区域最后一个数(num)一开始就不让它参与遍历,最后再让它归位
将最后一个位置的数作为num,我每次都将小于等于num的放在左边,大于num的数放在右面, 我将最后一位的数也就是等于num的数放在more中, 最后,将最后一位上的数和more交换, 因为more位置上的数一定是大于num中最靠左位置的,然后我将more和最后一位上的数,也就是等于num的数进行交换 此时就完成了所有的数小于靠左,大于靠右,等于靠中间
- 代码:见随机快排
存在的问题:总拿最后一个数去划分是和我的数据状况有关系的,
- 比如:[1,2,3,4,5,6],每次我都拿最后一个数去划分,每次也就只能搞定一个数,所以,时间复杂度就是O(N^2),
- 又比如我每次都正正好好打在中间,那么我的时间复杂度利用master公式得到就是O(N*logN)
随机快排
随机快速排序的细节和复杂度分析可以用荷兰国旗问题来改进快速排序时间复杂度O(N*logN),额外空间复杂度O(logN) 解释额外空间复杂度:使用额外空间这个事是一个概率,如果每次都打到中间,那么额外空间复杂度就是O(logN),但是数据状况是不确定的,所以长期期望是O(logN) 思路: 每次随机选择一个数,把它和最后一个位置上的数进行交换,然后拿这个随机的数做这样的划分 注意:工程上快排是非递归版本的 代码: public class QuickSort { public static void quicksort(int[] arr){ if(arr == null || arr.length < 2){ return ; } quicksort(arr, 0, arr.length-1); } public static void quicksort(int[] arr, int l, int r){ if(l < r){ int[] p = partition(arr, l, r); quicksort(arr, l, p[0] - 1); quicksort(arr, p[1] + 1, r); } } public static int[] partition(int[] arr, int l, int r){ swap(arr , (int)(Math.random() * (r - l + 1)) + l, r); int less = l - 1; int more = r; int cur = l; while(cur < more){ if(arr[cur] < arr[r]){ swap(arr , ++less, cur++); } else if(arr[cur] > arr[r]){ swap(arr , --more, cur); } else{ cur ++; } } swap(arr, r, more); return new int[]{ less + 1, more}; } public static void swap(int[] arr, int i, int j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main(String[] args){ int[] arr = {3, 6, 2, 5, 4, 3, 2}; quicksort(arr); System.out.println(Arrays.toString(arr)); } }
补充:有些算法我想绕开它本身的数据状况怎么办
- 我用随机这个东西打乱你的数据状况
- 哈希函数打乱
堆排
基础概念
二叉树两个概念
- 完全二叉树
- 满二叉树
堆的概念(堆就是完全二叉树)
大根堆:在完全二叉树中任何一个子树的最大值就是这个子树的头部
- 时间复杂度为log1 + log2 + ..... + log(N-1) = O(N),因为,每一个结点调整建立大根堆的过程,就是此时形成完全二叉树的高度,完全二叉树的高度就是logN
- 小根堆:在完全二叉树中任何一个子树的最小值就是这个子树的头部
数组对应脑海中的逻辑上的完全二叉树
i 左孩子: 2 * i + 1 右孩子: 2 * i + 2 父节点: (i - 1) / 2
-
- heapInsert(建立大根堆),当新添值后,不断往上走的过程
- heapify,当我某个值变小了,我要经历往下调整,小值往下扎,也是建立大根堆的过程
例:一个数据流中,随时可以取得中位数
- 思路
我建立两个堆,一个大根堆,一个小根堆,每当新吐出来一个数的时候, 我就将当前数和大根堆的堆顶比较,如果当前数小于等于大根堆堆顶,我才进去大根堆, 然后当大根堆和小根堆中数的个数差值大于1的话 (因为我要保证两个堆数量都趋近于n/2),我将数量多的那个堆的堆顶弹到另 个堆中,重新形成大根堆或者小根堆, 这样就保证了,较小的n/2在大根堆中,较大的n/2在小根堆中 这样就可以随时拿到中位数 利用堆的好处:每次我调整大根堆或者小根堆的代价只是O(logN)的,很低的复杂度
堆排
- 思路:
一个数组让它整体都形成一个大根堆,然后把最后一个位置和堆顶位置做交换,就相当于我形成一个大根堆,我每次都把这个大根堆的最后一个位置和头部交换,头部换回来之后,就把它从堆上减掉,然后我再调整缩小范围的这个堆,重新调整成一个大根堆,直到我堆的大小剪完,所有的最大值依次被填在了倒数第一,倒数第二......最后填到0位置,整个数组排完序
- 特点:这个结构已经形成了大小为n,进来一个数是代价O(logN)能够搞定的
代码:
package myP; import java.util.Arrays; public class HeapSort { public static void heapsort(int[] arr){ if(arr == null || arr.length < 2){ return ; } /*建立大根堆的过程·*/ for(int i = 0; i < arr.length; i++){ heapInsert(arr , i); } int heapsize = arr.length; /*将大根堆的头部与末尾的数进行交换,然后将换来的数一点一点往下沉,找到合适的位置,重新建立大根堆heapify*/ swap(arr, 0, --heapsize); while(heapsize > 0){ heapify(arr, 0, heapsize); swap(arr, 0, --heapsize); } } /*小值往下扎*/ public static void heapify(int[] arr, int index, int heapsize){ int left = index * 2 + 1; while(left < heapsize){ // largest就是记录我左右孩子中较大的数的位置 // 这句话的意思就是只有你左右孩子都在,并且不越界,并且右孩子的值比左孩子大,才会作为largest的取到的坐标值出现,否则的话就是左孩子 int largest = left + 1 < heapsize && arr[left + 1] > arr[left] ? left + 1:left; // 左右两个孩子之间的最大值和我之间哪个值大,哪个坐标就作为largest的值出现 largest = arr[index] > arr[largest] ? index : largest; // 当值还是我最大,那么什么都不变 if(largest == index){ break; } // 我的值小,交换我和较大孩子的值 swap(arr , index ,largest); // 将我的坐标和较大孩子的坐标交换,然后继续判断我要不要继续往下沉 index = largest; left = index * 2 + 1; } } /*建立大根堆,大值往上跑*/ public static void heapInsert(int[] arr, int index){ // 即使当index = 0的时候, -1/2也等于0 while(arr[index] > arr[( index - 1 ) / 2]){ swap(arr, index, ( index - 1 ) / 2); index = (index - 1) / 2; } } public static void swap(int[] arr, int i,int j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main(String[] args){ int[] arr = {1, 2, 4, 6, 3, 5, 8}; heapsort(arr); System.out.println(Arrays.toString(arr)); } }
排序的稳定性
- 概念:将数组排序之后,我能否保证原始的相对次序不变
所有排序的稳定性
冒泡排序: 可以实现成稳定的,当碰到相等的情况,我不换,继续往下走,就可以实现稳定的 插入排序: 可以实现成稳定的,当碰到相等的情况,我不换,继续往前走,就可以实现稳定的 选择排序: 无论怎样都做不到稳定性 归并排序: 可以实现成稳定的,遇到相等的先拷贝左边的 快排 : 做不到稳定性,因为partition做不到稳定性 堆排序 : 做不到稳定性,因为建立大根堆的过程就做不到稳定性
介绍一下工程中的综合排序算法
- 插排的优势,当整个数组长度小于60的情况下,直接用插排,因为样本量小的时候,插排的劣势显示不出来,反而插排的常数项很低,所以用插排
- 综合排序:综合排序过程中,里面如果放置的是基础类型就用快排(不用考虑稳定性),如果放置的是你自己定义的类型(就像学生的分数,年纪等,需要保证稳定性),就用归并排,当R - L < 60的时候就用插排
有关排序问题的补充
- 归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,可以搜“归并排序 内部缓存法”
- 快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜“01 stable sort”
有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官。面试官非良人。
- 奇和偶叫0-1标准,快排中按照值来划分,<=放左边,>放右边,也是0-1标准,因为快排很难做到稳定性,所以奇偶也很难做到
- partition在额外空间复杂度为O(1),时间复杂度为O(N)的情况下做不到稳定性,所以,快排和荷兰国旗问题在这样的情况下都做不到稳定性,因为都有partition
简略
- Code_09_Comparator
- 系统给你提供的一个有序的结构,你方便使用的时候,都会伴随着一个比较器的构造,比较器就是你在一个有序的结构中,你要怎么组织这个结构的,既可以做排序也可以做优先级队列,可以做TreeMap,重点就是:定义两个东西怎么比较大小
桶排序、计数排序、基数排序的介绍
- 非基于比较的排序,与被排序的样本的实际数据状况很有关系,所以实际中并不经常使用
- 时间复杂度O(N),额外空间复杂度O(N)
- 稳定的排序
补充例题
给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且要求不能用非基于比较的排序。
- 思路:
n个数准备n + 1个空桶,划分为n + 1 个空间,然后将数组中的数,符合哪个范围就放入哪个桶中,所以一定会产生一个空桶
而且我每次放一个数,桶只计算三个值,最小值,最大值,,记录是否为空桶
那么我们这里就杀死了一个可能性,就是我们这里求的最大差值一定不是来自于一个桶内部的数
因为一个桶中数最大的差值一定小于等于划分范围,然而空桶左侧非空桶的最大值,和空桶右侧非空桶的最小值差值就已经超过了这个划分范围, 代码:
public class MaxGap { public static int maxgap(int[] arr){ if(arr == null || arr.length < 2){ return 0; } int len = arr.length; int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; //找到数组中的最小值和最大值 for(int i = 0; i < len; i++){ max = Math.max(arr[i], max); min = Math.min(arr[i], min); } //如果最小值等于了最大值,那么说明数组中就一种数,都相等,最大差值就为0 if(max == min){ return 0; } //0号桶到n号桶的三个信息,用数组表示 boolean[] hasNum = new boolean[len + 1]; int[] mins = new int[len + 1]; int[] maxs = new int[len + 1]; int bid = 0; for(int i = 0; i < len; i ++){ //判断我这个数应该去几号桶 bid = bucket(arr[i] , len, min, max); //将这个桶的三个信息进行更新 mins[bid] = hasNum[bid] ? Math.min(mins[bid], arr[i]): arr[i]; maxs[i] = hasNum[bid] ? Math.max(maxs[bid], arr[i]): arr[i]; hasNum[bid] = true; } int res = 0; int lastIndex = maxs[0]; //找到每个非空桶和离它最近的左边非空桶,用当前的最小减去前一个的最大 for(int i = 1; i < len; i ++){ if(hasNum[i]){ res = Math.max(mins[i] - lastIndex, res); lastIndex = maxs[i]; } } return res; } public static int bucket(long num, long len, long min, long max) { return (int) ((num - min) * len / (max - min)); } public static void main(String[] args){ int[] arr = {1, 1, 7, 6, 5, 3}; System.out.println(maxgap(arr)); } }
- 思路:
用数组结构实现大小固定的队列和栈
思路:
假定数组大小为3 (1)实现栈 index代表如果用户想让我新加一个数,我应该把它放在什么位置 index如果在0位置,用户想让我给它弹出一个数,或者如果index在3的位置上用户还想再加一个数,那么我就提示报错 (2)实现队列 start变量:如果我要拿去一个数,我要把哪个位置上的数拿过来,并且只要start到底了,我就回到开头,否则就向下走一个 end变量:如果新加一个数,我应该把它填到哪个位置上,并且只要end到底了,我就回到开头,否则就向下走一个 size变量:约束start和end变量的行为,如果size没有到3,那么我就把用户新给我的那个数放在end的位置上去,如果size不等于0,我总把start指向位置的数给用户,end和start是独立的,解耦的
代码
public class Array_To_Stack_Queue { public static class ArrayStack{ private Integer [] arr; private Integer size; /*判断数组长度是否正确*/ public ArrayStack(int initsize){ if(initsize < 0){ throw new IllegalArgumentException("The init size is less than 0"); } /*初始化数组*/ arr = new Integer[initsize]; /*size是当前可以放东西的位置,可以看作是空的*/ size = 0; } /*返回栈顶存的东西*/ public Integer peek(){ if(size == 0){ return null; } return arr[size - 1]; } /*向当前位添加*/ public void push(int obj){ if(size == arr.length ){ throw new ArrayIndexOutOfBoundsException("This queue is full"); } arr[size++] = obj; } /*弹出栈顶位置的元素*/ public Integer pop(){ if(size == 0){ throw new ArrayIndexOutOfBoundsException("The queue is empty"); } return arr[--size]; } } public static class ArrayQueue{ private Integer [] arr; private Integer size; private Integer last; private Integer start; public ArrayQueue(int initsize){ if(initsize < 0){ throw new IllegalArgumentException("The init size is less than 0"); } arr = new Integer[initsize]; /*size表示我还没有没有空间放东西,就是数组满没满*/ size = 0; /*last表示我当前位置可以放东西。可以当作原本是空的*/ last = 0; /*指向当前栈顶元素*/ start = 0; } public void push(int obj){ if(size == arr.length){ throw new ArrayIndexOutOfBoundsException("This queue is full"); } size ++; arr[last] =obj; last = last == arr.length -1 ? 0: last + 1; } public Integer poll(){ if(size == 0){ throw new ArrayIndexOutOfBoundsException("The queue is empty"); } size --; int tmp = start; start = start == arr.length ? 0 : start + 1; return arr[tmp]; } } public static void main(String[] args) { } }
实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作。
要求
- pop、push、getMin操作的时间复杂度都是O(1)。
- 设计的栈类型可以使用现成的栈结构。
思路:
(1)第一种方法 准备两个栈,第一个栈就是Data栈,第二个就是min栈 我在压数的过程中,min栈随着Data一起增长,每当我压入一个数,我就让当前数和min栈的栈顶进行比较,如果当前数更小,min栈就压当前数,否则就重复压入一个min栈的栈顶,min栈的栈顶就是所有数中最小的那个数,弹出的过程中,同步弹出就可以了,Data弹一个,min也弹一个 (2)第二种方法 在第一种的基础上,min栈压入条件改变,当前数和min栈的栈顶进行比较,如果当前数小于等于栈顶的数,min栈就压当前数,否则不变,min栈的栈顶就是所有数中最小的那个数,弹出的过程中,当前的数如果和栈顶的数相等,那就两个都弹出,否则只弹出Data栈中的内容,min栈不变
代码:
public class GetMinStack { public static class MyStack{ /*两个栈,一个是数据栈,一个是存放最小值的那个栈(栈顶位置的数一定是最小的)*/ private Stack
stackData; private Stack stackMin; public MyStack(){ this.stackData = new Stack (); this.stackMin = new Stack (); } /*将新进来的数与stackMin的栈顶进行比较,要是小于或者等于,那在stackMin中也push newNum*/ public void push(int newNum){ if(this.stackMin.isEmpty()) { this.stackMin.push(newNum); }else if(newNum <= this.getMin()){ this.stackMin.push(newNum); } /*不管如何,最终的stackData中肯定是要push新的数的*/ this.stackData.push(newNum); } /*弹出数,要是弹出的数等于stackMin中栈顶的数,那就一起都带走,就是两个都弹出*/ public int pop(){ if(this.stackData.isEmpty()){ throw new RuntimeException("Your stack is empty."); } int value = this.stackData.pop(); if(value == this.getMin()){ this.stackMin.pop(); } return value; } /*得到stackMin这个栈 栈顶的位置*/ public int getMin(){ if(this.stackMin.isEmpty()){ throw new RuntimeException("Your stack is empty."); } // 只得到数值,不弹出 return this.stackMin.peek(); } } public static void main(String [] args){ MyStack stack = new MyStack(); stack.push(3); stack.push(4); stack.push(1); System.out.println(stack.getMin()); stack.pop(); System.out.println(stack.getMin()); stack.pop(); System.out.println(stack.getMin()); } }
如何仅用队列结构实现栈结构?(先进后出),如何仅用栈结构实现队列结构?(先进先出)
思路:
- 队列->栈
准备两个队列,一组数据进入队列(头进尾出),比如为[5, 4, 3, 2, 1], 5是头,1是尾,然后我要弹出一个数,也就是5,那么我将前面的数入另一个队列,最后一个别入,给用户返回了 - 栈->队列
push:用户给我的数永远进push栈
pop: 用户想让我给它一个数,永远从pop栈中拿
比如一组数1,2,3,4,5入栈push ->5, 4, 3, 2, 1,如果用户让我给他一个数,我先将数导入另一个栈pop中,变为->1, 2, 3, 4, 5,我从pop栈中给他拿
倒的过程两个注意点:
1.pop中得为空,不为空直接返回,倒不了
2.push得一次性倒完,什么时候倒完什么时候才停止,
- 队列->栈
代码:
import java.util.LinkedList; import java.util.Queue; import java.util.Stack; public class StackAndQueueConvert { /*用两个队列来实现栈结构 栈:push和pop 队列:push和poll */ public static class TwoQueuesStack{ private Queue
queue; private Queue help; public TwoQueuesStack() { queue = new LinkedList (); help = new LinkedList (); } public void push(int pushInt){ queue.add(pushInt); } public int pop(){ if(queue.isEmpty()){ throw new RuntimeException("Stack is empty!"); } /*只要是不剩最后一个,我就一直把queue中的内容倒到help中*/ while(queue.size() > 1){ help.add(queue.poll()); } /*我得先在这里存一下最后的结果,以便最后返回,不能直接返回,要不然就没法执行交换的操作了*/ int res = queue.poll(); /*交换,help变为queue,queue变为空的help队列,改变两个引用*/ swap(); return res; } public void swap(){ Queue tmp = queue; queue = help; help = tmp; } } public static class TwoStacksQueue{ //用两个栈来实现队列结构 private Stack stackPush; private Stack stackPop; public TwoStacksQueue(){ stackPush = new Stack (); stackPop = new Stack (); } public void push(int pushInt){ stackPush.push(pushInt); /*dao();随时随地都可以发生倒数据行为,只要满足了那两个条件*/ } public int poll(){ if(stackPop.isEmpty()&&stackPush.isEmpty()){ throw new RuntimeException("Queue is empty!"); } dao(); return stackPop.pop(); } public void dao(){ /*倒的过程两个注意点: 1.pop中得为空,不为空直接返回,倒不了 2.push得一次性倒完,什么时候倒完什么时候停止, 只要满足这两个条件什么时候都能发生倒数据行为,随时都可以! */ if(!stackPop.isEmpty()){ return; } while(!stackPush.isEmpty()){ stackPop.push(stackPush.pop()); } } } }
猫狗队列
题目:
宠物、狗和猫的类如下: public class Pet { private String type; public Pet(String type) { this.type = type; } public String getPetType() { return this.type; } } public class Dog extends Pet { public Dog() { super("dog"); } } public class Cat extends Pet { public Cat() { super("cat"); } } 实现一种狗猫队列的结构,
要求如下:
用户可以调用add方法将cat类或dog类的实例放入队列中; 用户可以调用pollAll方法,将队列中所有的实例按照进队列的先后顺序依次弹出; 用户可以调用pollDog方法,将队列中dog类的实例按照进队列的先后顺序依次弹出; 用户可以调用pollCat方法,将队列中cat类的实例按照进队列的先后顺序依次弹出; 用户可以调用isEmpty方法,检查队列中是否还有dog或cat的实例; 用户可以调用isDogEmpty方法,检查队列中是否有dog类的实例; 用户可以调用isCatEmpty方法,检查队列中是否有cat类的实例。
思路:
我准备两个队列,一个狗队列,一个猫队列 然后准备一个count,就是时间戳的意思
代码:
Code_04_DogCatQueue
转圈打印矩阵
题目:给定一个整型矩阵matrix,请按照转圈的方式打印它。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
打印结果为:1,2,3,4,8,12,16,15,14,13,9,5,6,7,11, 10
- 要求:额外空间复杂度为O(1)。
思路:
确定左上角和右下角,然后通过下标变化,顺时针打印出一圈,然后将左上角,右下角的坐标都往里圈移一个,接着顺时针打印里圈,知道左上角的行或者列大于右下角的行或者列,停止
代码:
public class PrintMatrixSpiralOrder { public static void spiralOrderPrint(int[][] matrix){ /*用两个点,四个坐标来打印第一圈矩形,再把两个点的坐标往里推一个,转圈打印里面一层的矩形,以此类推*/ int tR = 0;//上x int tC = 0;//上y int dR = matrix.length - 1;//下x int dC = matrix[0].length - 1;//下y while (tR <= dR && tC <= dC) { printEdge(matrix , tR++, tC++, dR--, dC--); } } public static void printEdge(int[][] matrix, int tR, int tC, int dR, int dC){ if(tC == dC){ for(int i = 0;i <= dR - tR; i ++ ){ System.out.print(matrix[tR+i][tC] + " "); } }else if(tR == dR){ for(int i = 0;i <= dC - tC; i ++ ){ System.out.print(matrix[tR][tC+i] + " "); } }else{ int curR = tR; int curC = tC; while(curC != dC){ System.out.print(matrix[tR][curC ++] + " "); } while(curR != dR){ System.out.print(matrix[curR ++][dC]+ " "); } while(curC != tC){ System.out.print(matrix[dR][curC --]+ " "); } while(curR != tR){ System.out.print(matrix[curR --][tC]+ " "); } } } public static void main(String[] args){ int[][] Matrix = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16} }; spiralOrderPrint(Matrix); } }
旋转正方形矩阵
- 题目:定一个整型正方形矩阵matrix,请把该矩阵调整成顺时针旋转90度的样子。
- 要求:额外空间复杂度为O(1)
思路:
(1)我找到左上,左下,右上,右下,四个点,现将它们四个进行交换,然后找左上右面第一个,再找到对应的三个点,将它们四个在进行交换,然后直到把第一圈换完 (2)将坐标变换,左上角向里圈移动一个,右下角向里圈移动一个,确定一个新的圈,在重复(1),直到所有交换完毕 比如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 我先找到1, 4, 16, 13,将它们四个进行交换, 然后来到下一位置,2, 8, 15, 9将它们四个也交换, 然后......,直到将第一圈所有数交换完毕, 来到里圈,6, 7, 11,10,将它们四个也交换,整 个图旋转完毕
代码:
package myP; public class RotateMatrix { /*整道题的思想一圈一圈转 首先,第一圈,只需要转第一行中除了最后一个的所有数就可以了,也就是dR - tR - 1个数就可以了 其次转多少圈:只要tR < dR那就一直转 */ public static void rotate(int [][] matrix){ int tR = 0;//上x int tC = 0;//上y int dR = matrix.length - 1;//下x int dC = matrix[0].length - 1;//下y while(tC< dC){ rotateEdge(matrix, tR++, tC++, dR--, dC--); } } public static void rotateEdge(int[][] matrix, int tR, int tC, int dR, int dC){ int times = dC - tC; int tmp = 0; // i就是找 出发点 for(int i = 0; i != times; i ++){ tmp = matrix[tR][tC + i]; matrix[tR][tC + i] = matrix[dR - i][tC]; matrix[dR - i][tC] = matrix[dR][dC- i]; matrix[dR][dC- i] = matrix[tR + i][dC]; matrix[tR + i][dC] = tmp; } } public static void printMatrix(int[][] matrix){ for(int i = 0;i < matrix.length; i ++){ for(int j =0; j < matrix[i].length; j ++){ System.out.print(matrix[i][j] + " "); } } } public static void main(String[] args){ int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, { 13, 14, 15, 16 } }; rotate(matrix); printMatrix(matrix); } }
之”字形打印矩阵
题目:给定一个矩阵matrix,按照“之”字形的方式打印这个矩阵,
例如: 1 2 3 4 5 6 7 8 9 10 11 12 “之”字形打印的结果为:1,2,5,9,6,3,4,7,10,11,8,12
- 要求:额外空间复杂度为O(1)
思路:
设计两个点,A,B都在(0, 0)位置上,一个boolean型变量,确定打印方向,向下还是向上 A的运动轨迹为每次向右移动,撞到最右了,向下移动 B的运动轨迹为每次向下移动,撞到最下了,向右移动 A,B各走各的,解耦的
代码:
package myP; public class ZigZagPrintMatrix { public static void printMatrixZigZag(int[][] matrix){ /*两个点(A,B)的坐标,一个向右边走,一个向下走*/ int aR = 0; int aC = 0; int bR = 0; int bC = 0; /*判断朝哪个方向走*/ boolean fromUp = true; int endR = matrix.length - 1; int endC = matrix[0].length - 1; /*我只要没到最后一个点,我就一直走,两个点都走*/ while(aR <= endR){ printLevel(matrix, aR, aC, bR, bC, fromUp); // 如果A的列数来到最后一列,我才往下走,否则我不变 aR = aC == endC ? aR + 1 : aR; // 判断A是否到最后一行 aC = aC == endC ? aC : aC + 1; bC = bR == endR ? bC + 1 : bC; bR = bR == endR ? bR : bR + 1; fromUp = !fromUp; } } public static void printLevel(int[][]matrix, int aR, int aC, int bR, int bC, boolean f){ // 根据boolean类型不同,确定打印方向 if(f){ while(bC != aC + 1){ System.out.print(matrix[bR--][bC++] + " "); } } else{ while(aR != bR + 1){ System.out.print(matrix[aR++][aC--] + " "); } } } public static void main(String[] args){ int[][] Matrix = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}}; printMatrixZigZag(Matrix); } }
在行列都排好序的矩阵中找数
题目:给定一个有N*M的整型矩阵matrix和一个整数K,matrix的每一行和每一列都是排好序的。实现一个函数,判断K是否在matrix中。
例如: 1 3 5 6 2 5 7 9 4 6 8 10 如果K为4,返回true;如果K为11,返回false。
- 要求:时间复杂度为O(N+M),额外空间复杂度为O(1)。
- 思路:
先从右上角找,右上角是6,说明6下面可能不会有4, 因为这是个相对有序的数组,然后向左移动,来到5的位置, 5比4大,所以5下面肯定也没有要找的数,然后来到3的位置, 因为4比3大,所以,3左面的数被淘汰了,往下走, 来到5, 5比4大,所以5下面的也都不用看,继续往左走...... 直到找到4
- 代码
- 重要概念:一道题最优解来自于题的数据状况,或者是它的问法
打印两个有序链表的公共部分
- 题目:给定两个有序链表的头指针head1和head2,打印两个链表的公共部分
- 思路:和外排相似,重点就是merge
判断一个链表是否为回文结构
题目:给定一个链表的头节点head,请判断该链表是否为回文结构。 例如:
1->2->1,返回true。 1->2->2->1,返回true。 15->6->15,返回true。 1->2->3,返回false。 1->2->3->2->1 返回true。
- 注意:
链表问题,能够下功夫的就是额外空间,因为时间复杂度都差不多是O(N),笔试中随意用,面试中,用额外空间复杂度为O(1)解决链表问题 - 第一种思路:
在遍历过程中把所有结点放入到栈中去,然后再遍历一遍,每遍历一个就从栈中拿出一个,每一步比对值都相等,那就是回文结构 - 第二种思路
和第一种差不多,就是我定义两个指针,快指针和慢指针,快指针一次走两步,慢指针一次走一步,快指针走完了,慢指针来到中点的位置上,然后将慢指针后面的数压栈,然后就是对比从栈里弹出的数,和原链表的数是否相等,其实空间省了一半,但是额外空间复杂度还是O(N) - 进阶: 如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
- 第三种思路:
一个快指针,一个慢指针,快指针走完,慢指针走到中点的位置上(奇数正好在中点,偶数找到两个中点的前一个位置),然后慢指针右半部分逆序,
比如1->2->3->2->1,快指针走完,慢指针来到3的位置上,然后将3的next改了,指向null,2的next原来指向1,改为指向3,同理1指向2
得到1->2->3<-2-<1(3指向null),然后两个变量从左右两边的1开始,每次共同走一步,比对是否一样,走到终点位置停,得到true或者fasle,不过注意的是,走到终点之后,我要把指针改回去,改为原来的样子
+ 代码:
```
package myP;
import java.util.Stack;
public class IsPalindromeList {
public static class Node{
public int value;
public Node next;
public Node(int data){
this.value = data;
}
}
/*1.利用额外空间(空间复杂度为O(n)),将链表的全部数据压到栈里,然后从头节点开始和栈每次弹出的数进行比对,有不一样的,就返回false*/
// need n extra space
public static boolean isPalindrome1(Node head) {
Stack stack = new Stack();
Node cur = head;
while(cur != null){
stack.push(cur);
cur = cur.next;
}
while(head != null){
if(head.value != stack.pop().value){
return false;
}
head = head.next;
}
return true;
}
/*2.利用额外空间(空间复杂度为O(n)),用两个指针,快指针一次跑两个,慢指针一次一个,当快指针走到了结尾,慢指针走到中点的位置
链表的后半部分数据压到栈里,然后从头节点开始和栈每次弹出的数进行比对,有不一样的,就返回false*/
// need n/2 extra space
public static boolean isPalindrome2(Node head) {
if(head == null || head.next == null){
return true;
}
Node fast = head;//为什么视频上是fast = head.next ?
Node cur = head;
while(cur.next != null && cur.next.next != null){
fast = fast.next;
cur = cur.next.next;
}
Stack stack = new Stack();
while(fast!= null){
stack.push(fast);
fast = fast.next;
}
while(!stack.isEmpty()){
if(head.value != stack.pop().value){
return false;
}
head = head.next;
}
return true;
}
public static boolean isPalindrome3(Node head){
if(head == null || head.next == null){
return false;
}
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 = n1.next是差不多改变遍历的位置,要是n1.next = n2 那就是改变链表指针了
n1.next = null;
Node n3 = null;
/*下面的while,就是将后半部分链表改变指针方向,执行完结果就是类似于1->2->3<-2<-1 */
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;/*将n1预先存在n3中,后面的归位要用到*/
n2 = head;
boolean res = true;
while(n2 != null && n1 != null){
if(n1.value != n2.value){
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
/*下面的while,将链表后半部分指针归位,执行完结果就是1->2->3->2->1*/
n1 = n3.next;
n3.next = null;
while(n1 != null){
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
public static void printLinkedList(Node node) {
System.out.print("Linked List: ");
while (node != null) {
System.out.print(node.value + " ");
node = node.next;
}
System.out.println();
}
public static void main(String [] args){
Node head = null;
head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
head.next.next.next = new Node(2);
head.next.next.next.next = new Node(1);
printLinkedList(head);
System.out.print(isPalindrome1(head) + " | ");
System.out.print(isPalindrome2(head) + " | ");
System.out.print(isPalindrome3(head) + " | ");
printLinkedList(head);
System.out.println("=========================");
}
}
```
将单向链表按某值划分成左边小、中间相等、右边大的形式
题目:
给定一个单向链表的头节点head,节点的值类型是整型,再给定一个 整 数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于 pivot 的节点,中间部分都是值等于pivot的节点,右部分都是值大于 pivot的节点。 除这个要求外,对调整后的节点顺序没有更多的要求。 例如:链表9->0->4->5- >1,pivot=3。 调整后链表可以是1->0->4->9->5,也可以是0->1->9->5->4。总 之,满 足左部分都是小于3的节点,中间部分都是等于3的节点(本例中这个部 分为空),右部分都是大于3的节点即可。对某部分内部的节点顺序不做 要求
- 思路:最快的方式就是找一个数组,里面存节点类型,然后借助荷兰国旗问题,重排,然后利用next都串起来,笔试中这么干
缺点:
- 第一:荷兰国旗不能实现稳定性
- 第二:需要准备额外的辅助空间
- 进阶:
思路:
这个题一定会传入两个参数,1是头结点,第二个是num 准备三个变量less,equal,more,值都为null,类型是结点类型的引用类型 一次遍历过程中, 找到第一个小于num的结点,让less等于那个节点, 找到第一个大于num的结点,让more等于那个节点, 找到第一个等于equal的结点,让equal等于那个节点, 如果小于或者大于或者等于的不存在,那就让对应的变量还为null 再遍历一遍链表,如果发现小于num的,我看它是不是less,如果它是less,不用管, 如果它不是less,让这个节点挂载less的next上,同理more和equal也这样做, 然后让less的尾部和equal的头部相连,让equal的头部的more的尾部相连,整个链表就串好了 扣边界,可能某个区域没结点,调用next会出错 其实就相当于一个大链表分成三个小链表,然后三个链表首尾相连
代码:
class_03:03-SmallerEqualBigger
复制含有随机指针节点的链表
题目:一种特殊的链表节点类描述如下:
public class Node { public int value; public Node next; public Node rand; public Node(int data) { this.value = data; } } Node类中的value是节点值,next指针和正常单链表中next指针的意义一样,都指向下一个节点, rand指针是Node类中新增的指针,这个指针可能指向链表中的任意一个节点,也可能指向null。 给定一个由Node节点类型组成的无环单链表的头节点head, 请实现一个 函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。 比如:1->next->2->next->3->next->null 1-random->3,2->random->1,3->random->null 这样一个链表的拷贝
思路:
利用hash表(需要额外空间复杂度为O(N)) 遍历节点,现将1-copy->1',将1和1'放入哈希表中去,1是key,1'是value, 然后利用1->next找到2,将2和2'也放入哈希表中去,同理,3也是 然后我再遍历一下链表,我先找到1,通过key-value方式拿到1',此时1'的next和random指针都是null, 因为1的next是可以找到2的,然后2通过哈希表找到2',所以此时1'的next就可以设置为2' 我可以通过1的random指针找到3,然后3通过哈希表找到3',然后1'的random的指针就是3' 此时1的next和random指针就都已经确定了, 然后通过1->next->2,找到2,依次进行这样的操作,整个链表拷贝完毕
补充hash表
用法
HashMap
map = new HashMap<>(); map.put("A", 12); map.put("B", 13); map.put("c", 14); System.out.println(map.containsKey("A")); //true System.out.println(map.get("A")); //12 - 特点: 不管是取数据,还是存数据,时间复杂度都是O(1)
代码:
class_03:03-copyListWithRand1
进阶:不使用额外的数据结构,只用有限几个变量,且在时间复杂度为 O(N)内完成原问题要实现的函数
思路:
原本: 1->next->2->next->3->next->null 1-random->3,2->random->1,3->random->null 现在 1->next->1'->next->2->next->2'->next->3->next->3'->next->null 我现在一次取出两个结点,1和1',1->random->3,3->next->3' 然后让1'->next->3'就完成了1'的next和random 同理2和2',3和3',最后将1',2',3'从大的链表里分离出来
代码:
class_03:03-copyListWithRand1
两个单链表相交的一系列问题
题目:
在本题中,单链表可能有环,也可能无环。 给定两个单链表的头节点 head1和head2,这两个链表可能相交,也可能不相交。 请实现一个函数, 如果两个链表相交,请返回相交的第一个节点; 如果不相交,返回null 即可。
思路:思考三个问题:
1. 如何判断单链表有环还是无环:准备loop1和loop2,写函数实现,如果这个单链表有环,我返回第一个入环的节点,如果这个单链表无环,我返回空 (1)实现 1.1 我准备一个哈希表,然后从head1开始通过next指针的方式去遍历 在遍历的过程中,把每一个节点都放入哈希map中去, 下一次转回来的时候,我就可以查到,原来我这个结点之前加入过map, 就能判断是否有环,第一个加入map的就是入环的结点 1.2 如果不用哈希表怎么实现: 准备两个指针,一个是快指针,一个是慢指针 快指针一次走两步,慢指针一次走一步, 如果快指针在走的过程中遇到空了,那么这个链表必然没环 如果链表有环,快慢指针必然相遇,并且相遇后,快指针回到head1头节点位置上 然后快指针变为一次走一步,那么最后快指针和慢指针一定会在第一个入环节点处相遇,这是结论 (2)注意: 单链表一个结点只有一个next,所以不可能出现,先是一个环后来支出去一个尾巴的结构 2. 怎么判断两个无环单链表第一个相交的节点(loop1 == null && loop2 == null) Y字型相交或者两个链表的尾相交 利用map实现: 将链表1的所有结点都放在map中去,然后遍历链表2 然后查链表2的结点在不在map中,第一个在的,就是第一个相交的结点 如果链表2都遍历完了,都没有在map中的结点,那么两个链表不相交 不用map实现: 四个变量,长度len1, 链表最后一个节点end1, 长度len2, 链表最后一个节点end2, 我分别遍历链表1和链表2,然后得到这4个值,先判断end1等不等于end2, 这个等于是判断内存地址是不是相等(是不是一个结点),不是判断值是不是相等, 如果end1和end2不相等,那么这两个链表不可能相交, 如果end1等于end2,则相交 ,但是end不一定是第一个相交的点,也可能出现Y字型相交, 这是就用到len1和len2了,判断它们两个谁大, 假如len1 = 100, len2 = 80,那么len1,先走20步 然后它俩同时走,一定会同时走到第一个相交的结点 3. 怎么判断两个有环单链表第一个相交的节点(loop1 != null && loop2 != null) 三种情况: 第一种:6 6 这样的两条环,不相交 第二种:Y型下面带一个圆圈, 第三种: 一个圈上面伸出去两条线,就像天线一样支出去的结构 思路: loop1 == loop2:第二种结构,那直接利用单链表判断Y的第一个交点就可以了, loop1 != loop2:第一种或者第三种 区分1还是3: 让loop1->next继续往下走,如果,loop1转了一圈又回到自己都没碰到loop2,那么是第一种结构, 如果loop1在next的过程中遇到了loop2就是第三种 此时返回loop1还是loop2作为第一个相交的结点都对 因为loop1是距离链表1更近的结点,loop2是距离链表2更近的结点 4. 一个有环一个无环,那么一定不会相交,因为这是单链表结构
- 要求:如果链表1的长度为N,链表2的长度为M,时间复杂度请达到 O(N+M),额外空间复杂度请达到O(1)。
代码:
实现二叉树的先序、中序、后序遍历,包括递归方式和非递归方式
题目描述:
比如:1->第一层 2 3->第二层 4 5 6 7 ->第三层
```
+ 思路:
```
递归方式:来到每个节点的顺序:1 2 4 4 4 2 5 5 5 2 1 3 6 6 6 3 7 7 7 3 1
先序遍历:中左(中左中左) 右
把打印的时机放在第一次来到这个结点的时候
1 2 4 5 3 5 7
中序遍历:左中(左中左中) 右
把打印的时机放在第二次来到这个结点的时候
后序遍历:左右(左右左右) 中
把打印的时机放在第三次来到这个结点的时候
非递归方式:
先序遍历:
准备一个栈,先压入头结点,然后弹出并输出值,
然后要是当前结点有右结点,就先压入右结点,就是 有右先压右,有左后压左,
这样最后弹出的顺序就是中,先左,后右
中序遍历:
当前结点为空,从栈中拿出一个变为当前结点,并打印,当前结点向右
当前结点不为空,当前结点压入栈中,当前结点往左
大致意思就是,我压一溜儿左边界,依次往外弹,再去遍历每 个弹出结点右孩子的过程
后序遍历:
仿照先序遍历,只不过变为先中再左再右这样入栈s1,
然后它的出栈就是先中右左,一个出栈s1,我把它放入另一个栈s2中
最后s2出栈的顺序就是左右中,也就是后序遍历
准备栈的原因:
因为栈的结构就是从上到下进入,那么从下到上弹出,
二叉树也是,我从上往下找结点,但是比如我的左结点找到我中间的结点,
我就需要往回找,队列就不适合了,栈符合这种结构
```
+ 代码:
```
class_04:Code_01_PreInPosTraversal
```
如何直观的打印一颗二叉树
代码:
class_04:Code_02_PrintBinaryTree 打印结果解释: 先脑子顺时针转90度去看 ^ ->代表左上方离我最近的结点是我的父结点 v ->代表左下方离我最近的结点是我的父结点
在二叉树中找到一个节点的后继节点
题目:现在有一种新的二叉树节点类型如下:
public class Node { public int value; public Node left; public Node right; public Node parent; public Node(int data) { this.value = data; } } 该结构比普通二叉树节点结构多了一个指向父节点的parent指针。 假设有一棵Node类型的节点组成的二叉树, 树中每个节点的parent指针都正确地指向 自己的父节点,头节点的parent指向null(有的指向自己)。 只给一个在二叉树中的某个节点 node,请实现返回node的后继节点的函数。 在二叉树的中序遍历的序列中,node的下一个节点叫作node的后继节点。 后继结点就是在中序遍历中,一个结点的下一个结点 前驱结点就是在中序遍历中,一个结点的上一个结点 比如: 1 2 3 4 5 6 7
思路:
中序遍历:4 2 5 1 6 3 7 找后继结点: 当一个结点有右子树(比如2),那么它的后继结点就是整个右子树上最左的结点(5) 当一个结点没有右子树(比如5),那么就找到底哪个结点, 这个5能作为 该结点左子树的 最后一个结点(1) 也就是结点x,x没有右子树,通过x的父指针找到x的父,如果x是父指针的右孩子, 那就继续往上,一直到某个结点是它父节点的左孩子, 那个父节点就是最初原始节点x的后继结点 找前驱结点: 当一个结点x如果有左子树,就是左子树上最右的结点就是它的前驱 当一个结点x如果没有左子树,就往上找,当前结点是它父结点的右孩子就停,那个父节点就是最初原始节点x的前驱结点
代码:
class_04: Code_03_SuccessorNode
介绍二叉树的序列化和反序列化
- 序列化:一个结构,我用什么方式能记录下来,记录的过程就叫序列化
- 反序列化:把一个内容如何还原成内存中的树结构
思路:
比如: 1 2 3 4 5 6 7 序列化: 先序遍历: 我把它存成一个字符串,下回我再建这个树通过字符串就够了 str: 1_2_4_#_#_5_#_#_3_6_#_#_7_#_#_ (_表示一种值的结尾,#表示遇到了null) 按层序列化: 1_2_3_4_5_6_7_#_#_#_#_#_#_#_#_ 反序列化: 先序遍历(见代码) 按层序列化:(代码)
- 代码:
class_04: Code_04_SerializeAndReconstructTree
判断一棵二叉树是否是平衡二叉树
- 平衡二叉树概念:在一棵树中的任何一个结点,它左子树和右子树的高度差不超过1
思路:
以每个结点为头的整颗子树是不是平衡的,每个都是平衡的,整棵树就是平衡的 整和信息:以x为结点的数 (1)x左子树是否平衡 (2)x右子树是否平衡 (3)左右平衡的基础上 x左子树高度 (4)左右平衡的基础上 x右子树高度
代码:
class_04: Code_06_IsBalancedTree的14-39行
判断一棵树是否是搜索二叉树(BST)
- 搜索二叉树定义:这棵树上任何一个结点为头的子树,左子树都比它小,右子树都比它大
- 思路:二叉树的中序遍历结点依次升序的就是搜索二叉树,通常搜索二叉树是不出现重复结点的,因为如果有重复结点的值,可以将所有的值压缩在一个结点的内部
- 代码:
class_04:Code_01_PreInPosTraversal下的inOrderUnRecur的78行 把打印的时机换成比较的时机,再用变量记录你上回拿到的值是啥,就能判断中序遍历是否升序
判断一棵树是否是完全二叉树(CBT)
- 思路:二叉树按层遍历
(1)如果一个结点它有右孩子,没有左孩子,一定不是完全二叉树 (2)在排除了第一个条件,那么它就剩两种情况,有左没右,左右都没有, 那么要求以后遇到的所有结点都必须是叶结点,否则,不是完全二叉树 如果遍历完了所有结点都不违反(1)(2),那么它是完全二叉树 可以看做我设置一个阶段,boolean类型,值为false,表示阶段未开启 当满足2条件则开启阶段,值变为true
代码:
class_04:Code_07_IsBSTAndCBT
已知一棵完全二叉树,求其节点的个数
- 要求:时间复杂度低于O(N),N为这棵树的节点个数
思路:
比如: 0 0 0 0 0 0 0 0 0 0 0 b 用一个变量h将结点数为n的二叉树的高度记录下来 (1)如果头结点右子树左边界是否到了最后一层(b这个结点存在的时候) 如果到了最后一层,那么左子树一定是满的,左树的结点数,2^3 - 1 加上当前结点 就是 2^3 - 1 + 1 = 2^3,然后递归求右树剩下的结点 (2)如果头结点右子树左边界没有到了最后一层(b这个结点不存在的时候) 那么右子树一定是高度为2的满二叉树,然后递归求左树剩下的结点
代码:
class_04:Code_08_CompleteTreeNodeNumber
时间复杂度为O(logN) * O(logN)
每一层只遍历一个结点,一共需要遍历O(logN)结点, 每个结点我还要遍历当前结点的左边界,又是O(logN),所以时间复杂度就是O(logN)^2
哈希
认识哈希函数和哈希表
哈希函数:有无数多种,经典的哈希函数
特点: (1)它的输入域input是无穷大的 (2)输出域output是有穷尽的S (3)输入参数是固定的,得到哈希函数的返回值一定是固定的,没有随机成分 (4)当输入不一样,也有可能得到输出一样的值, 就是会发生两个不同的输入对应一个输出,这我们叫做哈希碰撞 原因是输入域太大,输出域相对较小 (5)当你给我大量不同输入,我将在整个S域上均匀的出现它们的返回值 这就是哈希函数的离散型 就比如input ->0-99(为了举例子,正常是是无穷的) output->0,1,2 给我99个样本,依次计算对应的输出,基本上算完之后 0位置上30多个,1位置上30多个,3位置上30多个 特征: (1)哈希函数是和输入规律没关系的,可以用来打乱数据 (2)如果输出在s域上均匀分布,对m取余之后在0-m-1上也均匀分布 假如我有一个哈希函数,可以得到2^64的范围,其实就是一个16字节的字符串 ,每个位置0-9/A-F,每个位置都是相互独立的 此时我需要1000个相互独立哈希函数,我只要用这一个哈希函数就能改出1000个, 做法就是,我可以得到的16字节拆分为每个为8字节哈希函数分别为h1和h2 通过h1 + 1 * h2做出一个h3来 通过h1 + 2 * h2做出一个h4来 通过h1 + 3 * h2做出一个h5来 通过不停改系数,做出1000个哈希函数出来 好处: 哈希函数的大小和单样本的大小是没关系的
经典大数据问题:我有一个100T的大文件,文件是无序的,每一个行是一个字符串,要求把所有重复的字符串打印出来
思路:利用hash分流 我问:多少台机器 答:1000台 我问:大文件存在哪 答:存在分布式文件系统 我问:处理大文件按行读的工具 答:有,直接说怎么处理 我把机器都标好号,从1-999,然后我把每一行作为一个文本读出来 利用hash函数算出hashcode,hashcode%1000,将得到的值放入该位置对应的机器上 那么相同的文本一定会分配到同一台机器,因为hash函数性质,相同输入一定有相同的输出, 那么多少种不同的字符串(是类型,不是大小)会均匀的分布到1000台机器上, 然后再单独在一台机器上看有哪些重复的 如果1台机器上文件大小还是太大,那就继续利用hash函数分
哈希表
哈希表经典结构:
比如分配一个17的空间, 我使用hash表的时候我可以put(key1, value),也可以get(key1),还可以remove(key1)等 首先我进行put(key1, value)操作,我把key1拿出来算hashcode, key1->hashcode->code1,得到code1,我把它%17会得到0-16上的一个, 比如说得到10,那它就把这条记录挂到10位置的下面,那么显而易见肯定会有冲突的值 比如我又要put(key2, value2),经过计算得到的code2也是10, 那么我就在10的位置上看有没有key2,如果有那就更新key2的值, 如果没有key2,我就在key1的后面连接key2 再出现冲突就这么挂,那么我可以认为0-16位置上的链几乎是均匀往上涨的
离线扩容:
我再后台准备一个更大的区域去做扩容,用户put就两边都put, 用户get就从老的结构里get,就是为了不会让用户等 等新的扩容完成了,就把使用切到新的结构上,然后把老的结构废掉
在线扩容:
我用着用着发现超了,然后等待扩容,然后扩容完再返回结果,会让用户等
- hash表的增删改查,复杂度为O(1),在数学上不是绝对的,但是我们平常用的时候,可以当作O(1)
- JVM底层实现hash表是 用数组和红黑树(Treemap),每一个位置里面是一个红黑树,但是正常的都是用数组和单链表实现的
代码:
class_05:Code_01_HashMap
设计RandomPool结构
题目:
设计一种结构,在该结构中有如下三个功能: insert(key):将某个key加入到该结构,做到不重复加入。 delete(key):将原本在结构中的某个key移除。 getRandom():等概率随机返回结构中的任何一个key。
- 要求: Insert、delete和getRandom方法的时间复杂度都是O(1)
思路:
准备size变量,准备两张hash表,因为getRandom用一张hash表实现不了 原因:hash表的均匀分布是在大量数据的基础上,而且也不是严格一样, 比如我来了一个A,是第0号位置进来的,B是第一号位置进来的 (只是模拟map1和map2而已,实际放置的结构不知道是什么样子呢) map1 map2 key value key value A 0 0 A B 1 1 B 依次类推 map1 map2 key value key value A 0 0 A B 1 1 B ... ... ... ... Z 25 25 Z 然后利用Math.random * 26确定一个0-25的整数,然后从map2中找到数对应的值返回,保证随机等概率 这就实现了insert和getRandom功能 delete要考虑当删掉某条记录的时候,在map中会出现洞,就导致getRandom函数出问题 所以解决方法:当我删掉某条记录的时候,比如说是17位置的字符串, 我把map最后一位的值也就是Z,放在这个位置,也就是17的位置上, 然后把最后一个,也就是25删掉,
代码:
class_05: Code_02_RandomPool
认识布隆过滤器
基础认识:
- 就是比特类型的map
解决问题:黑名单问题
假定有一个黑名单,黑名单里有100亿个url,每个url是64字节, 当用户搜索某个url,如果在黑名单中就返回true,如果不属于这个黑名单就返回false 用hash相对代价有点高,但是也可以解决, 面试时候首先说经典解法,然后面试官要你优化的话 你可以去问面试官,这个系统允许有很低的失误么,允许的话,说布隆过滤器
- 应用场景:就是查某个东西是否在一个集合中,布隆过滤器其实就是一种集合
- 短板:有失误率(它是属于,如果你在黑名单中,一定不会返回false,但是会发生这样的情况,某个url其实不在黑名单中,但是它也给你返回了true了)
怎么实现 长度确定的bit数组->拿基础类型拼
//1个整型(int) = 4个字节 = 32个bit //1个长整型(long) = 8个字节 = 64个bit public static void main(String[] args){ //一个1000大小的桶,可以存32000bit int[]arr = new int[1000]; //32000 int index = 30000; //定位到30000在哪个桶 int intIndex = index / 32; //定位到桶里的哪个bit应该被描黑 int bitIndex = index % 32; System.out.println(intIndex); System.out.println(bitIndex); //(1 << bitIndex):1左移16位 //arr[intIndex] | (1 << bitIndex) num与 (1 << bitIndex)进行'或'运算 //这句话就是将30000对应bit位描黑 arr[intIndex] = (arr[intIndex] | (1 << bitIndex)); }
实现思路
布隆过滤器准备一个长度为m的比特类型数组[0 - m-1],和k个相互独立的hash函数 拿url1来举例,url1经过k个hash函数算出一个hash值code1,code2,....,codek, 然后依次对m取余,就能对应到m-1上面的很多个,将算出来的所有位置描黑 可能几个hash函数打到同一位置,那已经描黑的就让他继续变黑, 经过上面的过程,我们称这个url进到布隆过滤器中了 然后让以后的每个url(url2, url3,...)都进行相同的操作,此时数组中很多位置就被描黑了 当我查一个url的时候,我怎么知道它在不在这个黑名单中? 这个url经过k个hash函数,算出k个位置, 如果这k个位置都是黑的,我就说和url在这个黑名单中 如果有一个不是黑的,我就说这个url不在黑名单里 空间越大,失误率越低, 空间越小,失误率越高 (可能所有的位置都变黑了,这样的话我检查的每个url都在黑名单中了,失误率高) 空间开多大和什么有关系 1. 多少个url: 因为哈希函数的大小和单样本的大小是没关系的 比如 说我的url是64或者128字节或者256字节不影响布隆过滤器的大小 因为哈希函数只要能够接受这种参数就够了,不关系样本是什么 我只关心算出来的hashcode值, 所以哈希函数在设计的过程中,大小 开多大只和多少个url有关系 和单个url具体多少字节没关系,只要能够接受这种参数就够了
三个公式:
1. 空间计算公式m = (n * ln(p))/((ln2)^2)
n是样本量,p是预期失误率 m是需要空间的大小,单位为bit
除以8之后才是实际使用的字节,
G,M,K这种单位都是字节
可以将计算结果适当调大再代入公式3
2. 哈希函数个数 k = ln2* (m/n)
如果得到的不是整数,那就向上取整
3. 失误率 = (1 - e ^(-(n*k)/m))^ k
将更改过m和k代入求预期失误率
```
+ 面试套路:
```
1. 经典结构告诉他,他会说太费,然后自己问他允不允许有失误率,失误率是多少,他说允许
2. 讲布隆过滤器原理,url经过多个hash函数,相对位置描黑
3. 怎么检查url我,计算url位置,所有位置都是黑的,我认为在布隆过滤器中
4. 具体布隆过滤器参数的确定,三个公式,说哪个参数和什么变量有关
5. 确定m之后,比如说m是16G,那么我将它在范围内适当的往上调,为了让失误率更低一些,
6. 根据原始(不是调过之后的)的m值计算k,然后向上取整
7. 然后将调过的m和k值代入第三个公式,告诉面试官我的预期失误率
```
认识一致性哈希
经典服务器抗压结构
先有个前端,接受request ,任何一个request打到某个前端上都是无差别的 打到哪个前端服务器上提供的服务都是一样的, 后端有个寄存组,假设我现在有三台机器0, 1, 2 我现在想将"A" 31,存到服务里 首先打到前端上来,因为前端带相同的hash函数 然后A经过hash函数再模3之后,会得到0, 1,2中的一个值 假如得到的值为1,,那我就将A, 31存在1这个台机器上 因为不同的str,经过哈希函数算完之后均匀的得到0, 1, 2上的其中的一个,所以0, 1, 2这三台机器负载都均衡 那么我怎么查呢假如我想查A年龄是多少, 到前端还是一样,前端将这个hash值算出来,还是1 然后到1这台机器上拿A的年龄是多少,返回给前端,前端返回给用户 补充负载均衡: 我理解的负载均衡就是每个机器处理的东西差不多,cpu占用差不多 内存使用差不多,等等一些系统指标,包括网络I/O什么的 缺点:数据迁移不方便,加机器减机器不方便
上个问题用一致性哈希结构解决:一致性哈希结构可以把数据迁移的代价变得很低,同时又负载均衡
思路:
把整个哈系结构的返回值想象成一个环,0, 1, 2, 3, ......2^64, 0, 1, ..... 然后我假设有三台机器,把三台机器的ip或者host什么的拿出来计算hash值,然后对应环上的某个位置 现在将A,10这条记录存到某台机器上,将A这个key经过hash函数,会得到一个hash值,不进行模运算, 然后它会打到环上的某个位置,然后顺时针找到离这个位置最近的机器, 然后由这台机器存A, 10, 找的时候一样,计算hash值,找到位置,去顺时针离它最近的位置去取
实现
还是服务端三个服务器m1, m2, m3,前端还是无差别负载服务器 然后将m1, m2, m3的hash值排序做成一个数组, 现在将A, 10存入服务器,先到前端服务器,然后计算A对应的hash值, 然后去数组[m1, m2, m3]数组中找, 从左到右,二分法找数组中第一个>= A对应hash值 的那个位置就是存A,10的那台服务器 此时我怎么再加一台m4机器 将m4对应的hash值算出来,假如m4的hash值范围为m2 < m4 < m3, 那么我只需要将m2到m4之间的也就是原本属于m3的数据,迁移到m4就好了 此时我怎么减掉一台m4机器 我将m4机器上的内容迁移到m3身上就可以了
缺点
在机器数量比较少的时候,这个环是不能保证均分的 (均分是在大量数据的基础上) 即使均分了, 加减机器的时候可能会导致负载不均衡
解决缺点方法:虚拟节点技术
真实机器m-1, m-2, m-3 我不让m-1的ip去抢这个环,我给m1 分配1000个虚拟节点(m-1-1, m-1-2, m-1-3,......m-1-1000) 给m-3也分配1000个 我准备一张路由表,就是我从真实机器可以查它有哪些虚拟节点,从虚拟节点可以查到属于哪个物理机器 我让1000个虚拟节点去抢这个环,一共就是3000个虚拟结点, 每个虚拟节点所负责的域一律给它的物理机器去处理,这样首先能实现均分 这时,我将添加一台机器m-4,我给m4也分配1000个虚拟节点, 直接进环,每个虚拟节点就把它下一个数据拿到自己这来, 如果我要减掉一台机器,我根据路由表,减掉相应的节点
认识并查集结构(基础7的开头整个)
功能:
(1)非常快的解决两个元素是否属于一个集合 isSameSet(A, B) (2)两个元素各自所在的集合将它们合并在一起 Union(A, B)->A,B元素所在集合合并在一起
- 注意:首先并查集初始化的时候,必须把是一次性所有数据样本给他
自己实现:
1. 使用list完成,操作不方便 2. 1(转圈指向自己), 2(转圈指向自己), 3(转圈指向自己), 4(转圈指向自己), 5(转圈指向自己) (1)每个数自己形成一个小集合,自己是自己集合内的代表节点 (2)代表节点:一个集合中最头的那个节点的上一个指针是指向自己的,那个节点就是集合的代表结点 (3)找自己所在集合:例:2->1(转圈指向自己)<-3 比如说3找自己所在集合,就是自己不断往上指 指到1个这个节点它指向自己1了,我就说3所在集合的代表节点是1 (4)isSameSet(A, B):找两个元素A,B是否属于一个集合,就是A不断往上找,找到代表集合结点停, B也不断往上找,找到代表集合结点停,如果他俩代表节点是一个,表示它俩在一个集合里, 如果他俩代表节点不是一个,表示它俩不在一个集合里 (5)两个元素各自所在的集合将它们合并在一起 Union(A, B): 比如两个集合:2->1(转圈指向自己)<-3 5->4(转圈指向自己) 找A所在集合的代表结点,找到B所在集合的代表节点,然后找各自集合哪个元素多,然后我们把少元素的集合挂载多元素集合代表节点的下面 得到就是 2->1(转圈指向自己)<-4<-5 3(指向1) 优化: 比如4->3->2->1<-5 查找某个元素4的代表节点过程中,当我查找完成后,这个结构将被改写,从4开始往上的路径,沿途所有结点,把它的父节点打扁平,4下面的东西不管
岛问题
- 题目描述:一个矩阵中只有0和1两种值,每个位置都可以和自己的上、下、左、右四个位置相连,如果有一片1连在一起,这个部分叫做一个岛,求一个矩阵中有多少个岛?
举例:
0 0 1 0 1 0
1 1 1 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
这个矩阵中有三个岛。
```
+ 思路:
```
原问题不需要并查集结构,递归就可以搞定
但是如果这个矩阵特别大,给你多个cpu,设计一个分治的思路,多任务并行的方法把这道题搞定
递归方法:
用两个for循环从头开始遍历,for里嵌套一个感染函数,感染函数会把一片的1都变为2,岛的数量+1
感染函数是一个递归
多任务解题思路:
将矩阵分为多个部分,求各个部分的岛的数量,然后再合并,合并是重点,假如矩阵中岛的形状就是一个C,将矩阵从中间分为两半,左边是一个岛,右边是两个岛,重点就是怎么合并,让最后结果为正确结果就是一个岛
矩阵:
1 1 1 1 1 1 1 1
0 1 1 1 1 0 0 0
0 0 0 0 1 0 0 1
0 0 1 1 1 0 0 1
0 0 0 1 0 0 1 1
将矩阵分为两半
1 1 1 1 1 1 1 1
0 1 1 1 1 0 0 0
0 0 0 0 1 0 0 1
0 0 1 1 1 0 0 1
0 0 0 1 0 0 1 1
从(0, 0) 位置开始记录感染中心,导数量和感染边界
1 1 1 1->A C<-1 1 1 1
0 1 1 1->A C<-1 0 0 0
0 0 0 0 C<-1 0 0 1
0 0 1 1->B C<-1 0 0 1
0 0 0 1->B 0 0 1 1
左半部分:感染边界记录,岛的数量为2,
右半部分:感染边界记录,岛的数量为2,
一共4个,
开始查第一行的A所在集合和C所在集合是不是一个,如果没合并过,那么现在合并,岛的数量-1,
来到第二行,A所在集合和C所在集合是不是一个?是一个!,所以不减一,
继续第三行,0对1,不管
然后来到第四行,A所在集合和C所在集合是不是一个?不是一个!合并BC,然后减一
然后来到第五行,0对1,不管
最终得到岛的数量为2
所以只要记录一个岛的信息及四个边界的信息就可以实现合并了,要搞清楚两个合并
传递是怎么处理的?
把所有边界信息放在一个分布式内存里,分布式内存中维持一个并查集,用smart实现
```
+ 代码:
```
class05:Code_03_Islands
```
图
前缀树
介绍前缀树,何为前缀树? 如何生成前缀树?
生成前缀树(Tire树)
一个树只有头结点,然后我将"abc", "bce", "abd", "bef"加入到树中 方法:一个字符串在加的过程中,总是从头结点开始,依次看有没有沿途的路 如果有就复用,如果没有就建一条 应用:根据要求添加数据项实现 (1)查n个字符串中,是否某一个字符串是以"ab"开始的 判断有没有ab这个路径 (2)查加入的字符串中是否有"ab"这个字符串, 那么就要在每一个结点上加入一个数据项,记录有多少个字符串是以当前这个结点结尾的 比如说我加的是"abf",那么b结点的数据项是0 但是如果我加入的是"ab",那么b结点存的数据项是1, 所以加入一个数据项可以扩充很多功能,还看可以判断某个字符串加入了几次 (3)给你一给字符串,求有多少个字符串是以它作为前缀 再加一个数据项,每个结点被划过了多少次
代码:
class_07:Code_01_TrieTree 注意:在演示的时候,把字母放在边上好实现,不要放在结点上,很难搞
贪心策略
给定一个字符串类型的数组strs,找到一种拼接方式,使得把所有字符串拼起来之后形成的字符串具有最低的字典序
- 贪心的概念:贪心就是某种简洁的标准,在标准下给所有的东西分出个大小来,然后根据我定的优先级决定的顺序就是贪心
思路:
两个字符串:str1和str2 首先不能按照字符串各自的字典序排完之后拼接起来,得到的结果不一定对,'b', 'ba' ->'bba',没有'bab'小 排序策略: str1.str2(拼接) <= str2.str1 str1放前面 str1.str2(拼接) >= str2.str1 str2放前面 保证正确的比较策略,也就是我的比较策略是有传递性的 证明1: a.b < b.a b.c < c.b, 证明:a.c < c.a 解:如果把一个字符串比作一个k进制的数 两个字符串相加就是把前一个字符串向左移动 后一个字符串 那么多位 然后加上后一个字符串 比如a和b两个字符串变量相加,计算公式就是 a * k^(b长度) + b, 其中k^(b长度)记作m(b), 那么原式就可以化简为 a * m(b) + b < b * m(a) + a b * m(c) + c < c * m(b) + b 然后第一个式子两边同时-b 后 * c,第二个式子两边同时 -b 后 * a得到 a * m(b) * c < ( b * m(a) + a - b ) * c ( b * m(c) + c - b ) * a < c * m(b) * a 第一个式子左半部分,等二个式子右半部分,然后合并,化简最终得到结果为 a * m(c) + c < c * m(a) + a 证明2:在得到的序列中任意两个字符串交换后,都会产生更大的字典序 (1)...... a m1 m2 m3 m4, ......b (2)...... b m1 m2 m3 m4, ......a 证明:(2)比(1)只可能大,不可能小 解:第一个式子,因为a < m1, 所以 m1 a > a m1 所以 m1 m2 a > a m1 m2,所以一直把a往后交换得到 ......m1 m2 m3 ......a b > ...... a m1 m2 m3 m4, ......b 然后将b往前交换得到: ......b m1 m2 m3 ......a > ...... a m1 m2 m3 m4, ......b
代码:
class_07:Code_05_LowestLexicography
-
题目描述:一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的 金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金 条,怎么分最省铜板?
例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60 金条要分成10,20,30三个部分。 如果, 先把长度60的金条分成10和50,花费60 再把长度50的金条分成20和30,花费50 一共花费110铜板。 但是如果, 先把长度60的金条分成30和30,花费60 再把长度30金条分成10和20,花费30 一共花费90铜板。输入一个数组,返回分割的最小代价。 典型哈夫曼编码问题
思路:
把数据建成小根堆比如:{1, 2, 3, 4} 取数组中最小的两个合并,1 和 2 结果返回3, 放到原数组中{3, 3, 4} 再取数组中最小的两个合并,3 和 3 结果返回6, 放到原数组中{6, 4} 然后再取数组中最小的两个合并,6 和 4 结果返回10, 放到原数组中{10} 最终结果为10
代码:
class_07:Code_02_Less_Money
-
题目:
输入: 参数1:正数数组costs 参数2:正数数组profits 参数3:正数k 参数4: 正数costs[i],表示i号项目的花费 profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润) k表示你不能并行、只能串行的最多做k个项目 w表示你初始的资金 说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个 项目。 输出: 你最后获得的最大钱数。 简化题意: 项目花费数组:costs 项目利润数组:profits w 是启动资金,只能做一个项目,不可以将w分分,然后分别做项目 k 你总共,最多可以做k个项目
思路:
按照项目分类,一个项目有两个信息,costs和profits 然后将项目按照cost建立小根堆 在小根堆中依次弹出头部,只要花费比w低的,都弹出来,然后放到一个大根堆中 大根堆根据profit建立,那么把大根堆的头部的那个项目就是收益最高的项目 然后把我的收益(costs + profits)加入初始资金w中,再去解锁用这些资金可以做的项目 然后再建立大根堆......, 一直做k个结束
代码:
class_07:Code_03_IPO
4.
+ 题目:
```
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面 是一个个具体的项目)
你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次
```
+ 思路:
```
先看最早结束的项目,然后淘汰掉因为做这个项目不能的项目
然后再看早结束的项目,然后再淘汰掉因为做这个项目不能的项目
然后依次进行,得到的项目就是最多的
```
+ 代码:
```
class_07:Code_06_BestArrange
```
介绍递归和动态规划
暴力递归:
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程 4,不记录每一个子问题的解
动态规划
- 从暴力递归中来
- 将每一个子问题的解记录下来,避免重复计算
- 把暴力递归的过程,抽象成了状态表达
- 并且存在化简状态表达使其更加简洁的可能
题目精解
求n!的结果
思路:
n! = n * (n - 1)! (n - 1)! = (n - 1) * (n - 2)! (n - 2)! = (n - 2) * (n - 3)! ...... 1! = 1
代码:
class_08:Code_01_Factorial
汉诺塔问题:打印n层汉诺塔从最左边移动到最右边的全部过程
汉诺塔:
最简单的例子: 三个杆:左, 中, 右, 左杆上套着3个圆盘,圆盘按照大小顺序套在杆上, 例如左杆上,最底层是3,,第二层是2,,最上面的一层是1,这样摆放 原则:不能大压小,只能小压大, 要求:利用三个杆完成将左杆上的圆盘都挪到右上,而且符合原则的挪动 提升:如果左杆上有N层,怎么从左 彻底挪到最右,打印所有步骤
思路:
不考虑左中右杆的问题,我用from to help 来记录 要求:把from杆上的内容挪到to上面去,中间借助help 第一步:把from上的 1 - n-1挪到help上去 第二步:把 from剩下 单独的n 挪到to上去 第一步:把help上的 1 - n-1挪到to上去
代码:
public static void process(int N, String from, String to, String help){ if(N == 1){ System.out.println("Move " + N + " from " + from + " to " + to); } else{ process(N-1, from, help, to); System.out.println("Move " + N + " from " + from + " to " + to); process(N-1, help, to, from); } } public static void main(String[] args) { process(3, "zuo", "you", "zhong"); }
打印一个字符串的全部子序列,包括空字符串
+ 题目:比如:"abc",所有子序列是"a", "b", "c", "ab", "bc", "ac, "abc"
思路:
a b c 代表三个位置,0, 1, 2 最初, res = "" 从0 位置开始 我可以选择往下传入一个a,或者"", 此时res 为 a 或者 "" 从1 位置开始 我可以选择往下传入一个b,或者"", 此时res 为 ab / b / a / "" 从3 位置开始 我可以选择往下传入一个c,或者"", 此时res 为 abc / bc / ac / c / ab / b / a / ""
代码:
母牛每年生一只母牛,新出生的母牛成长三年后也能每年生一只母牛,假设不会死。求N年后,母牛的数量。
思路:
f(n) = f(n - 1) + f(n - 3) f(n - 1): 去年的牛 f(n - 3): 3年前成熟的牛成熟后新生出来的牛
给你一个二维数组,二维数组中的每个数都是正数,要求从左上角走到右下角,每一步只能向右或者向下。沿途经过的数字要累加起来。返回最小的路径和。
思路:
暴力枚举:(不是贪心) 三种情况: (1)走到了最后一行:最短路径就是,当前值 + 右一位置到右下角的最短路径 (2)走到了最后一列:最短路径就是,当前值 + 下一位置到右下角的最短路径 (3)就是不在二维数组的最后一行和最后一列 那么有两个选择 向下走(递归求最短路径),或者向右走(递归求最短路径) 取两者之间到右下角最短的那个,再加上自身值 缺点: 比如: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 我 (0 , 1) 位置算最短路径的时候,我需要(1, 1) 和(0, 2) 我 (1 , 0) 位置算最短路径的时候,我需要(1, 1) 和(2, 0) 此时我就产生了重复计算(1, 1)的位置,像这样的重复状态还是很多的 所以这是暴力不行的原因 考虑:如果可以有一种机制,把某个位置的返回值记录下来做一个缓存 就像刚才的(1, 1),我把它的返回值记录下来, 然后再去使用的时候直接从缓存中去拿就会省空间了 无后效性问题:与到达这个状态的路径是没有关系的,只要状态参数确定了,返回参数一定确定 有后向性问题:比如说汉诺塔问题我要求打印所有步骤,你之前做出的选择必然会影响后续的解 什么样尝试版本的递归可以改成动态规划: 当递归展开的过程中,发现有重复状态,而且重复状态与到达它的路径是没有关系的(无后效性问题) 那么一定可以改成动态规划 步骤: (1)写出尝试版本(暴力版本) (2)分析可变参数,哪几个可变参数可以代表返回值的状态,可变参数是几维的,它就是一张几维表 (3)看需要的状态时哪一个,在表中点出来, (4)把你完全不依赖的值设置好,把basecase值设置好 (5)看一个普遍位置需要哪些位置,逆着回去就是我填表的位置 比如:这里不太准确是自己写的,原文件请看:基础8的2:20:57 [0, 2, 3, 4] [1, 2, 3, 4] [1, 2, 3, 4] [1, 2, 3, 5] 尝试版本(暴力) -> 见代码 可变参数:i, j 需要状态(0, 0)位置也就0 完全不依赖的值:最后一行,最后一列(从右下角往左推,和往上推) [0, 2, 3, 17] [1, 2, 3, 13] [1, 2, 3, 9] [11, 10, 8, 5] basecase值:(3, 3)位置也就是5 普遍位置:比如(2, 2),需要右面和下面的值,比较哪个小,由图可知,下面比较下,所以取下面的值,(2, 2)位置的值也就变为了11,然后依次往上找最后推出的(0, 0)位置的值就是答案
- 代码:
class_08:Code_07_MinPath {
给你一个数组arr,和一个整数aim。如果可以任意选择arr中的数字,能不能累加得到aim,返回true或者false
思路:
基础8的2:51:27 如果这道题降成一维的,叫动态规划的空间优化,空间优化也是要计算这么多位置的 只是它只用一个数组这么干,注意:这不叫一维的时间优化,是二维的空间优化
问题?:怎么确定的无后效性问题
基础8的2:44:52
有负数咋办:
基础8的2:59:52
代码:
class_08 : Code_08_Money_Problem