基础讲解
一、认识复杂度和简单排序算法
1.如何交换两个数据
public void swap(int[] arr, int i, int j){ // 当i和j是一个位置的时候会出错
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
2.一堆数据中只有一个数出现的次数为奇数次,其余都为偶数次,如何求出这个出现奇数次的数
// 由于异或运算的性质,出现偶数次的数进行异或后的结果为零,所以全部数异或后的结果就是出现奇数次的那个数
public void printOddTimesNum(int[] arr){
int eor = 0;
for(int num : arr){
eor ^= num;
}
System.out.println(eor);
}
3.一堆数据中有两个数出现的次数为奇数次,其余都为偶数次,如何求出这两个出现奇数次的数
做法:假设a和b是两个出现奇数次的数,则所有数据的异或结果eor就是ab,如果ab的结果eor不为0,则eor上至少有一位是1。那么a和b对应位置上一个是0一个是1,再将全部数据分为该位置为0和为1的两组数据,将该位置都为0或者都为1的数据进行异或,得到a或者b,再将得到的结果与eor异或得到另一个数。
public void printOddTimesNum(int[] arr){
int eor = 0;
for(int num : arr){
eor ^= num;
}
//若eor!=0,则eor必然有一个位置上是1
int rightOne = eor & (~eor + 1); //提取出最右的1
int onlyOne = 0;
for(int num : arr){
if((num & rightOne) == 0){ //将对应位置上是0的数全部进行异或,即可得到一个奇数次的数
onlyOne ^= num;
}
}
System.out.println(onlyOne + "," + (eor^onlyOne));
}
4.递归行为时间复杂度的估算
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)
二、排序算法
1.选择排序
时间复杂度O(n^2),空间复杂度O(1),不稳定
思路:
- 每一轮选择,找到最大(最小)的元素,并把它交换到合适的位置
//选择最小值
public void selectionSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int i=0;i<arr.length-1;i++){
int minIndex=i;
for(int j=i+1;j<arr.length;j++){
minIndex=arr[j]<arr[minIndex]?j:minIndex;
}
swap(arr,i,minIndex);
}
}
public void swap(int[] arr,int i,int j){
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
//选择最大值
public void selectionSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int right=arr.length-1;right>0;right--){
int max=right;
for(int left=0;i<right;i++){
max=arr[max]<arr[left]?left:max;
}
if(max!=right){
swap(arr,max,right);
}
}
}
2.冒泡排序
时间复杂度O(n^2),空间复杂度O(1),稳定
思路:
- 每轮冒泡不断地比较相邻的两个元素,如果他们是逆序的,则交换它们的位置
- 下一轮冒泡,可以调整未排序的右边界,减少不必要的比较
public void bubbleSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
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 void bubbleSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
int j=arr.length-1;
while(true){
int x=0; //用于确定右边界
for(int i=0;i<j;i++){
if(arr[i]>arr[i+1]){
swap(arr,i,i+1);
x=i;
}
}
j=x;
if(j==0){
break;
}
}
}
3.插入排序
时间复杂度O(n^2),空间复杂度O(1),稳定
思路:
- 将数组分为两部分,[0...low-1]已排序好区域,[low...arr.length-1]未排序好区域
- 每次从未排序区域取出low位置的元素插入到已排序区域
public 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);
}
}
}
public void insertionSort(int[] arr){
for(int low=1;low<arr.length;low++){
//将low位置的元素插入至[0...low-1]已排序好区域
int t=arr[low];
int i=low-1;//已排序区域指针
while(i>=0&&t<a[i]){
arr[i+1]=arr[i];
i--;
}
//找到插入位置
if(i!=low-1){
arr[i+1]=t;
}
}
}
4.归并排序
时间复杂度O(N*logN),空间复杂度O(N),稳定
思路:
- 分 - 每次从中间切一刀,处理的数据少一半
- 治 - 当数据仅剩一个时可以认为有序
- 合 - 两个有序的结果,可以进行合并排序
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, 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;
int 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) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
public class MergeSortTopDown {
/*
a1 原始数组
i~iEnd 第一个有序范围
j~jEnd 第二个有序范围
a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while (i <= iEnd && j <= jEnd) {
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void sort(int[] a1) {
int[] a2 = new int[a1.length];
split(a1, 0, a1.length - 1, a2);
}
private static void split(int[] a1, int left, int right, int[] a2) {
int[] array = Arrays.copyOfRange(a1, left, right + 1);
// System.out.println(Arrays.toString(array));
// 2. 治
if (left == right) {
return;
}
// 1. 分
int m = (left + right) >>> 1;
split(a1, left, m, a2); // left = 0 m = 0 9
split(a1, m + 1, right, a2); // m+1 = 1 right = 1 3
// 3. 合
merge(a1, left, m, m + 1, right, a2);
System.arraycopy(a2, left, a1, left, right - left + 1);
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
public class MergeSortBottomUp {
/*
a1 原始数组
i~iEnd 第一个有序范围
j~jEnd 第二个有序范围
a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while (i <= iEnd && j <= jEnd) {
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void sort(int[] a1) {
int n = a1.length;
int[] a2 = new int[n];
for (int width = 1; width < n; width *= 2) {
for (int i = 0; i < n; i += 2 * width) {
int m = Integer.min(i + width - 1, n - 1);
int j = Integer.min(i + 2 * width - 1, n - 1);
System.out.println(i + " " + m + " " + j);
merge(a1, i, m, m + 1, j, a2);
}
System.arraycopy(a2, 0, a1, 0, n);
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
5.快速排序
快速排序的整体思路:
1.假设基准数字是x,每一次排序完达到的效果是小于等于x的数都在数组的左边,大于x的数都在数组的右边。
2.先划定一个小于等于的范围,初始右边界在-1处;划定一个大于的范围,初始左边界在arr.length处。
3.从左往右开始遍历每一个数,如果arr[i]<=x,就让arr[i]跟小于等于的右边界的下一个数进行交换,右边界向右移一位,i++;如果arr[i]>x,就让arr[i]跟大于的左边界的前一个数进行交换,左边界向左移一位,i不动。
4.等所有的数都遍历完了,小于等于x的就都在左边,大于x的都在右边。
快排1.0,选择的基准数字为数组最右的那个数,将数组分为两个部分,≤x、>x。
快排2.0,选择的基准数字为数组最右的那个数,将数组分为三个部分,<x、=x、>x。
快排3.0,选择的基准数字是数组中随机选取的一个数字,将选取的数跟数组的最后一个位置交换,将数组分为三个部分,<x、=x、>x。
现在用的都是快排3.0,时间复杂度O(N*logN),空间复杂度O(logN)。
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) {
swap(arr, l + (int) (Math.random() * (r - l + 1)), 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) {
int less = l - 1;
int more = r;
while (l < more) {
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 };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
6.堆排序
堆结构:
(1)堆结构就是用数组实现的完全二叉树结构
(2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
(3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
(4)优先级队列结构底层就是堆结构
public static class MyMaxHeap {
private int[] heap;
private final int limit;
private int heapSize;
public MyMaxHeap(int limit) {
heap = new int[limit];
this.limit = limit;
heapSize = 0;
}
public boolean isEmpty() {
return heapSize == 0;
}
public boolean isFull() {
return heapSize == limit;
}
public void push(int value) {
if (heapSize == limit) {
throw new RuntimeException("heap is full");
}
heap[heapSize] = value;
// value heapSize
heapInsert(heap, heapSize++);
}
// 用户此时,让你返回最大值,并且在大根堆中,把最大值删掉
// 剩下的数,依然保持大根堆组织
public int pop() {
int ans = heap[0];
swap(heap, 0, --heapSize);
heapify(heap, 0, heapSize);
return ans;
}
// 新加进来的数,现在停在了index位置,请依次往上移动,
// 移动到0位置,或者干不掉自己的父亲了,停!
private void heapInsert(int[] arr, int index) {
// [index] [index-1]/2
// index == 0
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
// 从index位置,往下看,不断的下沉
// 停:较大的孩子都不再比index位置的数大;已经没孩子了
private void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) { // 如果有左孩子,有没有右孩子,可能有可能没有!
// 把较大孩子的下标,给largest
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
// index和较大孩子,要互换
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
堆排序:
(1)先让整个数组都变成大根堆结构,建立堆的过程
从上到下的方法,时间复杂度为O(N*logN)
从下到上的方法,时间复杂度为O(N)
(2)把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为O(N*logN)
(3)堆的大小减成0之后,排序完成。
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 size = arr.length;
swap(arr, 0, --size); //把头结点跟堆末尾的值交换
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
//上浮操作
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) { //如果子节点的值大于父节点的值,交换
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
//下潜操作
public static void heapify(int[] arr, int index, int size) {
int left = 2 * index + 1; //找到左孩子
while (left < size) {
int largest = left + 1 < size && arr[left] < arr[left + 1] ? left + 1 : left; //如果右孩子也在范围内,并且右孩子比左孩子大,那么较大的那个就是右孩子,否则就是左孩子
largest = arr[index] > arr[largest] ? index : largest;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = 2 * index + 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 sortedArrDistanceLessK(int[] arr, int k) {
if (k == 0) {
return;
}
// 默认小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index = 0;
// 0...K-1
for (; index <= Math.min(arr.length - 1, k - 1); index++) {
heap.add(arr[index]);
}
int i = 0;
for (; index < arr.length; i++, index++) {
heap.add(arr[index]);
arr[i] = heap.poll();
}
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
}
7.计数排序
// only for 0~200 value
public static void countSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int[] bucket = new int[max + 1];
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]]++;
}
int i = 0;
for (int j = 0; j < bucket.length; j++) {
while (bucket[j]-- > 0) {
arr[i++] = j;
}
}
}
8.基数排序
// only for no-negative value
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
public static int maxbits(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int res = 0;
while (max != 0) {
res++;
max /= 10;
}
return res;
}
// arr[L..R]排序 , 最大值的十进制位数digit
public static void radixSort(int[] arr, int L, int R, int digit) {
final int radix = 10;
int i = 0, j = 0;
// 有多少个数准备多少个辅助空间
int[] help = new int[R - L + 1];
for (int d = 1; d <= digit; d++) { // 有多少位就进出几次
// 10个空间
// count[0] 当前位(d位)是0的数字有多少个
// count[1] 当前位(d位)是(0和1)的数字有多少个
// count[2] 当前位(d位)是(0、1和2)的数字有多少个
// count[i] 当前位(d位)是(0~i)的数字有多少个
int[] count = new int[radix]; // count[0..9]
for (i = L; i <= R; i++) {
// 103 1 3
// 209 1 9
j = getDigit(arr[i], d);
count[j]++;
}
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
for (i = R; i >= L; i--) {
j = getDigit(arr[i], d);
help[count[j] - 1] = arr[i];
count[j]--;
}
for (i = L, j = 0; i <= R; i++, j++) {
arr[i] = help[j];
}
}
}
public static int getDigit(int x, int d) {
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
9.总结
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
冒泡排序 | O(N^2) | O(1) | 稳定 |
选择排序 | O(N^2) | O(1) | 不稳定 |
插入排序 | O(N^2) | O(1) | 稳定 |
随即快排 | O(N*logN) | O(logN) | 不稳定 |
堆排序 | O(N*logN) | O(1) | 不稳定 |
归并排序 | O(N*logN) | O(N) | 稳定 |
计数排序 | O(N) | O(N) | 稳定 |
基数排序 | O(N) | O(N) | 稳定 |
10.排序算法总结
1.不基于比较的排序,对样本数据有严格要求,不易改写
2.基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3.基于比较的排序,时间复杂度的极限是O(N*logN)。
4.时间复杂度O(N*logN)、额外空间复杂度低于O(N),且稳定的基于比较的排序是不存在的。
5.为了绝对的速度选快排,为了省空间选堆排,为了稳定性选归并
11.常见的坑
1.归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,有兴趣可以搜“归并排序 内部缓存法”,但是会变得不稳定
2.“原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O(N^2)
3.快速排序可以做到稳定性问题,但是非常难,不需要掌握, 可以搜“01stable sort”,但是对样本数据要求更多
4.有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,时间复杂度做到O(N),额外空间复杂度做到O(1),碰到这个问题,可以怼面试官。(类似于随机快排,就是01partation,原始的随机快排都不是稳定的)
三、二叉树的遍历
1.先序(前序)遍历
- 递归方式
public void preOrder(TreeNode root){
if(root==null){
return;
}
System.out.println(root.val);
preOrder(root.left);
preOrder(root.right);
}
- 非递归方式
public void preOrder(TreeNode root){
if(root!=null){
Stack<TreeNode> stack=new Stack<>();
stack.add(root);
while(!stack.isEmpty()){
root=stack.pop();
System.out.println(root.val);
if(root.right!=null){
stack.push(root.right);
}
if(root.left!=null){
stack.push(root.left);
}
}
}
}
public void preOrder(TreeNode root){
Stack<TreeNode> stack=new Stack<>();
TreeNode cur=root;
while(!stack.isEmpty()||cur!=null){
if(cur!=null){
stack.push(cur);
System.out.println(cur.val);
cur=cur.left;
}else{
TreeNode pop = stack.pop();
cur=pop.right;
}
}
}
2.中序遍历
- 递归方式
public void inOrder(TreeNode root){
if(root==null){
return;
}
preOrder(root.left);
System.out.println(root.val);
preOrder(root.right);
}
- 非递归方式
public void inOrder(TreeNode root){
if(root!=null){
Stack<TreeNode> stack=new Stack<>();
while(!stack.isEmpty()||root!=null){
if(root!=null){
stack.push(root);
root=root.left;
}else{
root=stack.pop();
System.out.println(root.val);
root=root.right;
}
}
}
}
public void inOrder(TreeNode root){
Stack<TreeNode> stack=new Stack<>();
TreeNode cur=root;
while(!stack.isEmpty()||cur!=null){
if(cur!=null){
stack.push(cur);
cur=cur.left;
}else{
TreeNode pop=stack.pop();
System.out.println(pop.val);
cur=pop.right;
}
}
}
3.后序遍历
- 递归方式
public void postOrder(TreeNode root){
if(root==null){
return;
}
preOrder(root.left);
preOrder(root.right);
System.out.println(root.val);
}
- 非递归方式
public void postOrder(TreeNode root){
if(root!=null){
Stack<TreeNode> stack1=new Stack<>();
Stack<TreeNode> stack2=new Stack<>();
stack1.push(root);
while(!stack1.isEmpty()){
root=stack1.pop();
stack2.push(root);
if(root.left!=null){
stack1.push(root.left);
}
if(root.right!=null){
stack1.push(root.right);
}
}
while(!stack2.isEmpty()){
System.out.println(stack2.pop().val);
}
}
}
public void postOrder(TreeNode root){
Stack<TreeNode> stack=new Stack<>();
TreeNode cur=root;
TreeNode pop = null;
while(!stack.isEmpty()||cur!=null){
if(cur!=null){
stack.push(cur);
cur=cur.left;
}else{
TreeNode peek=stack.peek();
if(peek.right==null||peek.right==pop){
pop=stack.pop();
System.out.println(pop.val);
}else{
cur=peek.right;
}
}
}
}
4.如何判断一个二叉树是搜索二叉树
- 中序非递归方式
public boolean isValidBST(TreeNode root){
TreeNode p=root;
Stack<TreeNode> stack=new Stack<>();
long prev=Long.MIN_VALUE;
while(p!=null||!stack.isEmpty()){
if(p!=null){
stack.push(p);
p=p.left;
}else{
TreeNode pop=stack.pop();
if(prev>=pop.value){
return false;
}
prev=pop.value;
p=pop.right;
}
}
return true;
}
- 中序递归方式
public boolean isValidBST(TreeNode root){
if(root==null){
return true;
}
return doValidBST(new AtomicLong(Long.MIN_VALUE),root);
}
public boolean doValidBST(AtomicLong prev,TreeNode root){
if(root==null){
return true;
}
boolean isValidLeft=doValidBST(prev,root.left);
if(prev.get()>=root.val){
reture false;
}
prev.set(root.val);
boolean isValidRight=doValidBST(prev,root.right);
return isValidLeft&&isValidRight;
}
- 上下限递归
public boolean isValidBST(TreeNode root){
if(root==null){
return true;
}
return doValidBST(root,Long.MIN_VALUE,Long.MAX_VALUE);
}
public boolean doValidBST(TreeNode node,Long min,Long max){
if(node==null){
return true;
}
if(node.val<=min||node.val>=max){
return false;
}
return doValidBST(node.left,min,node.val)&&doValidBST(node.right,node.val,max);
}
- 递归方式
public static class ReturnData{
public boolean isBST; //是否是搜索二叉树
public int max; //最大值
public int min; //最小值
public ReturnData(boolean isBST,int max,int min){
this.isBST=isBST;
this.max=max;
this.min=min;
}
}
public static ReturnData process(TreeNode x){
if(x==null){
return null;
}
ReturnData leftData=process(x.left);
ReturnData rightData=process(x.right);
int min=x.value;
int max=x.value;
if(leftData!=null){
min=Math.min(leftData.min,min);
max=Math.max(leftData.max,max);
}
if(rightData!=null){
min=Math.min(rightData.min,min);
max=Math.max(rightData.max,max);
}
boolean isBST=false;
if((leftData!=null?(leftData.isBST&&leftData.max<x.value):true) &&(rightData!=null?(rightData.isBST&&rightData.min>x.value):true)){
isBST=true;
}
return new ReturnData(isBST,max,min);
}
5.如何判断一棵二叉树是完全二叉树
思路:
- 一个节点有右孩子无左孩子,返回false
- 在与第一条不冲突的情况下,遇到第一个左右孩子不全的节点后,其后面的节点全是叶子节点
public boolean isCBT(TreeNode root){
if(root==null){
return true;
}
boolean leaf=false; //判断是否遇到第一个左右孩子不全的节点
LinkedList<TreeNode> queue=new LinkedList<>();
TreeNode l=null; //用于表示左孩子
TreeNode r=null; //用于表示右孩子
queue.add(root);
while(!queue.isEmpty()){
root = queue.poll();
l=root.left;
r=root.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;
}
6.如何判断一棵二叉树是满二叉树
public static class Info{
public int height;
public int nodes;
public Info(int h,int n){
this.height=h;
this.nodes=n;
}
}
public static Info process(TreeNode x){
if(x==null){
return new Info(0,0);
}
Info leftData=process(x.left);
Info rightData=process(x.right);
int height=Math.max(leftData.height,rightData.height)+1;
int nodes=leftData.nodes+rightData.nodes+1;
return new Info(height,nodes);
}
public static boolean isFull(TreeNode head){
if(head==null){
return true;
}
Info data=process(head);
return data.nodes==((1<<data.height)-1);
}
7.如何判断一棵二叉树是平衡二叉树
public static class ReturnType{
public int height;
public boolean isBalanced;
public ReturnType(boolean isBalanced,int height){
this.isBalanced=isBalanced;
this.height=height;
}
}
public static ReturnType process(TreeNode 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)+1;
boolean isBalanced=leftData.isBalanced&&rightData.isBalanced&&Math.abs(leftData.height-rightData.height)<2;
return new ReturnType(isBalanced,height);
}
8.给定两个二叉树的节点node1和node2,找到他们的最低公共祖先节点
public static void process(TreeNode head,HashMap<TreeNode,TreeNode> fatherMap){ //得到每一个节点的父亲节点
if(head==null){
return;
}
fatherMap.put(head.left,head);
fatherMap.put(head.right,head);
process(head.left,fatherMap);
process(head.right,fatherMap);
}
public static TreeNode lca(TreeNode head,TreeNode o1,TreeNode o2){
HashMap<TreeNode> fatherMap=new HashMap<>();
fatherMap.put(head,head);
process(head,fatherMap);
HashSet<TreeNode> set1=new HashSet<>();
TreeNode cur=o1;
while(cur!=fatherMap.get(cur)){
set1.add(cur);
cur=fatherMap.get(cur);
}
set1.add(head);
//接下来,遍历o2的父亲节点,判断是否与set1中的相等,返回第一个相等的节点即为最低公共祖先节点,若没有相等的,则返回head
}
//第一种情况,o1是o2的最低公共祖先,或者o2是o1的最低公共祖先
//第二种情况,o1和o2不互为最低公共祖先,需要向上查找
public static TreeNode lowestAncestor(TreeNode head,TreeNode o1,TreeNode o2){
if(head==null||head==o1||head==o2){
return head;
}
TreeNode left=lowestAncestor(head.left,o1,o2);
TreeNode right=lowestAncestor(head.right,o1,o2);
if(left!=null&&right!=null){
return head;
}
return left!=null?left:right;
}
//若 p,q 在 a 的两侧,则 ancestor 就是它们的最近公共祖先,此方法只适用于二叉搜索树
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode a=root;
while(p.val<a.val&&q.val<a.val || p.val>a.val&&q.val>a.val){
if(p.val<a.val){
a=a.left;
}else if(p.val>a.val){
a=a.right;
}
}
return a;
}
9. 在二叉树中找一个节点的后继节点
public TreeNode getSuccessorNode(TreeNode node){
if(node==null){
return node;
}
if(node.right!=null){
return getLeftMost(node.right);
}else{
TreeNode parent = node.parent;
while(parent!=null&&node!=parent.left){ //当前节点是父亲节点的右孩子
node=parent;
parent=node.parent;
}
return parent;
}
}
//找到最左的孩子
public TreeNode getLeftMost(TreeNode node){
if(node==null){
return node;
}
while(node.left!=null){
node=node.left;
}
return node;
}
10.二叉树的序列化和反序列化
就是内存里的一棵树如何变成字符串形式,又如何从字符串形式变成内存里的树
//将树变成字符串
public String serialByPre(TreeNode head){
if(head==null){
return "#_";
}
String res=head.value+"_";
res+=serialByPre(head.left);
res+=serialByPre(head.right);
return res;
}
//将字符串变成树
public TreeNode 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 TreeNode reconPreOrder(Queue<String> queue){
String value=queue.poll();
if(value.equals("#")){
return null;
}
TreeNode head=new TreeNode(Integer.parseInt(value));
head.left=reconPreOrder(queue);
head.right=reconPreOrder(queue);
return head;
}
11.折纸问题
请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。给定一个输入参数N,代表纸条都从下边向上方连续对折N次。请从上到下打印所有折痕的方向。
思路:
- 每一次的折纸都在上一次的折痕上方形成一条凹折痕,下方形成一条凸折痕
public void printAllFolds(int N){
printProcess(1,N,true);
}
//递归过程,来到了某一个节点,i是节点的层数,N一共的层数,down==true 凹 down==false 凸
public 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);
}
12.前中后序的统一写法
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();
if(root!=null){
stack.push(root);
}
while(!stack.isEmpty()){
TreeNode node=stack.peek();
if(node!=null){
stack.pop();
if(node.right!=null){
stack.push(node.right);
}
if(node.left!=null){
stack.push(node.left);
}
stack.push(node);
stack.push(null); //用null来作为标记
}else{
stack.pop(); //把栈顶的null标记给弹出
TreeNode pop=stack.pop();
result.add(pop.val);
}
}
return result;
}
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();
if(root!=null){
stack.push(root);
}
while(!stack.isEmpty()){
TreeNode node=stack.peek();
if(node!=null){
stack.pop();
if(node.right!=null){
stack.push(node.right);
}
stack.push(node);
stack.push(null); //用null来作为标记
if(node.left!=null){
stack.push(node.left);
}
}else{
stack.pop(); //把栈顶的null标记给弹出
TreeNode pop=stack.pop();
result.add(pop.val);
}
}
return result;
}
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();
if(root!=null){
stack.push(root);
}
while(!stack.isEmpty()){
TreeNode node=stack.peek();
if(node!=null){
stack.pop();
stack.push(node);
stack.push(null); //用null来作为标记
if(node.right!=null){
stack.push(node.right);
}
if(node.left!=null){
stack.push(node.left);
}
}else{
stack.pop(); //把栈顶的null标记给弹出
TreeNode pop=stack.pop();
result.add(pop.val);
}
}
return result;
}
四、图
1.图的基础要素的定义
(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;
}
}
(2)节点
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<>();
}
}
(3)图
import java.util.HashMap;
import java.util.HashSet;
public class Graph {
public HashMap<Integer,Node> nodes;
public HashSet<Edge> edges;
public Graph() {
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
(4)构建图
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);
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.图的宽度优先遍历
思路:
- 利用队列实现
- 从源节点开始依次按照宽度进队列,然后弹出
- 每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
- 直到队列变空
public static void bfs(Node node){
if(node==null){
return;
}
Queue<Node> queue=new Queue<>();
HashSet<Node> set=new HashSet<>();
queue.add(node);
set.add(node);
while(!queue.isEmpty()){
Node cur = queue.poll();
System.out.println(cur.value);
for(Node next:cur.nexts){
if(!set.contains(next)){
set.add(next);
queue.add(next);
}
}
}
}
3.图的深度优先遍历
思路:
- 利用栈实现
- 从源节点开始把节点按照深度放入栈,然后弹出
- 每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
- 直到栈变空
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);
set.add(next);
System.out.println(next.value);
break;
}
}
}
}
4.拓扑排序算法
适用范围:要求有向图,且有入度为0的节点,且没有环
public static List<Node> sortedTopology(Graph graph){
HashMap<Node,Integer> inMap=new HashMap<>(); //key:某一个节点 value:剩余的入度
Queue<Node> zeroInQueue=new LinkedList<>(); // 用于存放入度为0的节点
for(Node node:graph.nodes.values()){
inMap.put(node,node.in);
if(node.in==0){
zeroInQueue.add(node);
}
}
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;
}
5.kruskal算法(最小生成树算法)
适用范围:要求无向图
特点:从边出发考虑
算法步骤:
1 初始化。将所有边都按权值从小到大排序,将每个节点集合号都初始化为自身编号。
2 按排序后的顺序选择权值最小的边(u,v)。
3 如果节点 u 和 v 属于两个不同的连通分支,则将边(u,v)加入边集 TE 中,并将两个连通分支合并。
4 如果选取的边数小于 n-1,则转向步骤2,否则算法结束。
// Union-Find Set
public static class UnionFind {
private HashMap<Node, Node> fatherMap; //记录父亲节点
private HashMap<Node, Integer> rankMap; //记录集合的大小
public UnionFind() {
fatherMap = new HashMap<Node, Node>();
rankMap = new HashMap<Node, Integer>();
}
private Node findFather(Node n) {
Node father = fatherMap.get(n);
if (father != n) {
father = findFather(father);
}
fatherMap.put(n, father);
return father;
}
public void makeSets(Collection<Node> nodes) {
fatherMap.clear();
rankMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
rankMap.put(node, 1);
}
}
public boolean isSameSet(Node a, Node b) {
return findFather(a) == findFather(b);
}
public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aFather = findFather(a);
Node bFather = findFather(b);
if (aFather != bFather) {
int aFrank = rankMap.get(aFather);
int bFrank = rankMap.get(bFather);
if (aFrank <= bFrank) {
fatherMap.put(aFather, bFather);
rankMap.put(bFather, aFrank + bFrank);
} else {
fatherMap.put(bFather, aFather);
rankMap.put(aFather, aFrank + bFrank);
}
}
}
}
public static class EdgeComparator implements Comparator<Edge> { //根据边的权重升序排列
@Override
public int compare(Edge o1, Edge o2) {
return o1.weight - o2.weight;
}
}
public static Set<Edge> kruskalMST(Graph graph) {
UnionFind unionFind = new UnionFind();
unionFind.makeSets(graph.nodes.values());
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator()); //优先级队列,用于将边进行排序
for (Edge edge : graph.edges) {
priorityQueue.add(edge);
}
Set<Edge> result = new HashSet<>();
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
if (!unionFind.isSameSet(edge.from, edge.to)) { //如果边的两端不属于同一个集合,才能选这条边,选完之后将边的两端加入集合中
result.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return result;
}
6.prim算法(最小生成树算法)
适用范围:要求无向图
特点:从节点出发考虑
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<>(); //用于不让节点重复加入
Set<Edge> result = new HashSet<>();
for (Node node : graph.nodes.values()) {
if (!set.contains(node)) {
set.add(node);
for (Edge edge : node.edges) {
priorityQueue.add(edge);
}
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll(); //将优先级队列中权重最小的边弹出
Node toNode = edge.to;
if (!set.contains(toNode)) { //如果边的另一端节点没有加入过set,则这条边可选,将另一端节点加入set,然后再从另一端节点开始选边
set.add(toNode);
result.add(edge);
for (Edge nextEdge : toNode.edges) {
priorityQueue.add(nextEdge);
}
}
}
}
}
return result;
}
// 请保证graph是连通图
// graph[i][j]表示点i到点j的距离,如果是系统最大值代表无路
// 返回值是最小连通图的路径之和
public static int prim(int[][] graph) {
int size = graph.length;
int[] distances = new int[size];
boolean[] visit = new boolean[size];
visit[0] = true; //选取0节点为初始节点
for (int i = 0; i < size; i++) {
distances[i] = graph[0][i];
}
int sum = 0;
for (int i = 1; i < size; i++) {
int minPath = Integer.MAX_VALUE;
int minIndex = -1;
for (int j = 0; j < size; j++) {
if (!visit[j] && distances[j] < minPath) {
minPath = distances[j];
minIndex = j;
}
}
if (minIndex == -1) {
return sum;
}
visit[minIndex] = true;
sum += minPath;
for (int j = 0; j < size; j++) {
if (!visit[j] && distances[j] > graph[minIndex][j]) {
distances[j] = graph[minIndex][j];
}
}
}
return sum;
}
7.Dijkstra算法(单源最短路径算法)
适用范围:没有权值为负数的边(不能有累加和为负数的环)
public static HashMap<Node, Integer> dijkstra1(Node head) {
//从head出发到所有点的最小距离
//key:从head出发达到key
//value:从head出发达到key的最小距离
//如果在表中,没有T的记录,含义是从head出发到T这个点的距离为正无穷
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(head, 0);
//已经求过距离的节点,存在selectedNodes中,以后再也不碰
HashSet<Node> selectedNodes = new HashSet<>();
Node minNode = getMinDistanceAndUnselectedNode(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);
}
distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
}
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()) {
Node node = entry.getKey();
int distance = entry.getValue();
if (!touchedNodes.contains(node) && distance < minDistance) {
minNode = node;
minDistance = distance;
}
}
return minNode;
}
8.Bellman-Ford(含负权图的单源最短路径算法)
public class BellmanFord {
public static void main(String[] args) {
// 正常情况
/*Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
Vertex v5 = new Vertex("v5");
Vertex v6 = new Vertex("v6");
v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
v2.edges = List.of(new Edge(v4, 15));
v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
v4.edges = List.of(new Edge(v5, 6));
v5.edges = List.of();
v6.edges = List.of(new Edge(v5, 9));
List<Vertex> graph = List.of(v4, v5, v6, v1, v2, v3);*/
// 负边情况
/*Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 1));
v2.edges = List.of(new Edge(v3, -2));
v3.edges = List.of(new Edge(v4, 1));
v4.edges = List.of();
List<Vertex> graph = List.of(v1, v2, v3, v4);*/
// 负环情况
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
v1.edges = List.of(new Edge(v2, 2));
v2.edges = List.of(new Edge(v3, -4));
v3.edges = List.of(new Edge(v4, 1), new Edge(v1, 1));
v4.edges = List.of();
List<Vertex> graph = List.of(v1, v2, v3, v4);
bellmanFord(graph, v1);
}
private static void bellmanFord(List<Vertex> graph, Vertex source) {
source.dist = 0;
int size = graph.size();
// 1. 进行 顶点个数 - 1 轮处理
for (int i = 0; i < size - 1; i++) {
// 2. 遍历所有的边
for (Vertex s : graph) {
for (Edge edge : s.edges) { //遍历s节点的每一条边
// 3. 处理每一条边
Vertex e = edge.linked;
if (s.dist != Integer.MAX_VALUE && s.dist + edge.weight < e.dist) {//如果s节点的距离不是无穷大,并且s节点的距离加上边的权重小于e节点的距离,就更新e节点的距离,把e节点的上一个节点设为s
e.dist = s.dist + edge.weight;
e.prev = s;
}
}
}
}
for (Vertex v : graph) {
System.out.println(v + " " + (v.prev != null ? v.prev.name : "null"));
}
}
}
如果在【顶点-1】轮处理完成后,还能继续找到更短距离,表示发现了负环
9.Floyd-Warshall(多源点之间最短路径的算法)
public class FloydWarshall {
public static void main(String[] args) {
Vertex v1 = new Vertex("v1");
Vertex v2 = new Vertex("v2");
Vertex v3 = new Vertex("v3");
Vertex v4 = new Vertex("v4");
v1.edges = List.of(new Edge(v3, -2));
v2.edges = List.of(new Edge(v1, 4), new Edge(v3, 3));
v3.edges = List.of(new Edge(v4, 2));
v4.edges = List.of(new Edge(v2, -1));
List<Vertex> graph = List.of(v1, v2, v3, v4);
/*
直接连通
v1 v2 v3 v4
v1 0 ∞ -2 ∞
v2 4 0 3 ∞
v3 ∞ ∞ 0 2
v4 ∞ -1 ∞ 0
k=0 借助v1到达其它顶点
v1 v2 v3 v4
v1 0 ∞ -2 ∞
v2 4 0 2 ∞
v3 ∞ ∞ 0 2
v4 ∞ -1 ∞ 0
k=1 借助v2到达其它顶点
v1 v2 v3 v4
v1 0 ∞ -2 ∞
v2 4 0 2 ∞
v3 ∞ ∞ 0 2
v4 3 -1 1 0
k=2 借助v3到达其它顶点
v1 v2 v3 v4
v1 0 ∞ -2 0
v2 4 0 2 4
v3 ∞ ∞ 0 2
v4 3 -1 1 0
k=3 借助v4到达其它顶点
v1 v2 v3 v4
v1 0 -1 -2 0
v2 4 0 2 4
v3 5 1 0 2
v4 3 -1 1 0
*/
floydWarshall(graph);
}
static void floydWarshall(List<Vertex> graph) {
int size = graph.size();
int[][] dist = new int[size][size];
Vertex[][] prev = new Vertex[size][size];
// 1)初始化
for (int i = 0; i < size; i++) {
Vertex v = graph.get(i); // v1 (v3)
Map<Vertex, Integer> map = v.edges.stream().collect(Collectors.toMap(e -> e.linked, e -> e.weight));
for (int j = 0; j < size; j++) {
Vertex u = graph.get(j); // v3
if (v == u) {
dist[i][j] = 0;
} else {
dist[i][j] = map.getOrDefault(u, Integer.MAX_VALUE);
prev[i][j] = map.get(u) != null ? v : null;
}
}
}
print(prev);
// 2)看能否借路到达其它顶点
/*
v2->v1 v1->v?
dist[1][0] + dist[0][0]
dist[1][0] + dist[0][1]
dist[1][0] + dist[0][2]
dist[1][0] + dist[0][3]
*/
for (int k = 0; k < size; k++) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
// dist[i][k] + dist[k][j] // i行的顶点,借助k顶点,到达j列顶点
// dist[i][j] // i行顶点,直接到达j列顶点
if (dist[i][k] != Integer.MAX_VALUE &&
dist[k][j] != Integer.MAX_VALUE &&
dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
prev[i][j] = prev[k][j];
}
}
}
// print(dist);
}
print(prev);
}
static void path(Vertex[][] prev, List<Vertex> graph, int i, int j) {
LinkedList<String> stack = new LinkedList<>();
System.out.print("[" + graph.get(i).name + "," + graph.get(j).name + "] ");
stack.push(graph.get(j).name);
while (i != j) {
Vertex p = prev[i][j];
stack.push(p.name);
j = graph.indexOf(p);
}
System.out.println(stack);
}
static void print(int[][] dist) {
System.out.println("-------------");
for (int[] row : dist) {
System.out.println(Arrays.stream(row).boxed()
.map(x -> x == Integer.MAX_VALUE ? "∞" : String.valueOf(x))
.map(s -> String.format("%2s", s))
.collect(Collectors.joining(",", "[", "]")));
}
}
static void print(Vertex[][] prev) {
System.out.println("-------------------------");
for (Vertex[] row : prev) {
System.out.println(Arrays.stream(row).map(v -> v == null ? "null" : v.name)
.map(s -> String.format("%5s", s))
.collect(Collectors.joining(",", "[", "]")));
}
}
}
如果在 3 层循环结束后,在 dist 数组的对角线处(i==j 处)发现了负数,表示出现了负环
五、前缀树
何为前缀树?
它属于多叉树结构,典型应用场景是统计、保存大量的字符串,经常被搜索引擎系统用于文本词频统计。它的优点是利用字符串的公共前缀来减少查找时间,最大限度的减少无谓字符串的比较和存储空间
1.前缀树中节点的定义
public static class TrieNode{
public int pass; //表示该节点被经过多少次
public int end; //表示有多少字符串是以该节点结尾的
public TrieNode[] nexts;
public TrieNode(){
pass=0;
end=0;
nexts=new TrieNode[26]; //nexts[0]==null 没有走向0节点的路 nexts[0]!=null 有走向0节点的路
}
}
2.前缀树的定义
public static class Trie{
private TrieNode root;
public Trie(){
root = new TrieNode();
}
//插入一个字符串
public void insert(String str){
if(str==null){
return;
}
char[] chs=str.toCharArray(); //将字符串转成一个个字符
TrieNode node=root;
node.pass++;
int index=0; //用于标识是哪个字符,a对应0,b对应1,以此类推
for(int i=0;i<chs.length;i++){ //从左往右遍历字符
index=chs[i]-'a'; //得到字符对应的int值
if(node.nexts[index]==null){ //如果当前节点到index节点没有路径,则新建一个节点
node.nexts[index]=new TrieNode();
}
node=node.nexts[index]; //node移动到下一个节点
node.pass++;
}
node.end++;
}
//查询一个字符串之前加入过多少次,
public int search(String str){
if(str==null){
return 0;
}
char[] chs=str.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;
}
//在所有加入的字符串中,有几个是以pre这个字符串作为前缀的
public int prefixNumber(String pre){
if(pre==null){
return 0;
}
char[] chs=str.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.pass;
}
//删除某一个字符串
public void delete(String str){
if(search(str)!=0){ //确保树中有该字符串
char[] chs=str.toCharArray();
TrieNode node=root;
node.pass--;
int index=0;
for(int i=0;i<chs.length;i++){
index=chs[i]-'a';
if(--node.nexts[index].pass==0){
node.nexts[index]=null; //只有java能这样写,C++需要遍历到底去析构
return;
}
node=node.nexts[index];
}
node.end--;
}
}
}
六、贪心算法
贪心算法:在某一个标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫做贪心算法。也就是说,不从整体最优上加以考虑,所做出的是在某种意义上的局部最优解。
贪心算法在笔试时的解题套路
- 实现一个不依靠贪心策略的解法x,可以用最暴力的尝试
- 脑补出贪心策略A,贪心策略B,贪心策略C.....
- 用解法x和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
- 不要去纠结贪心策略的证明
贪心策略在实现时,经常使用到的技巧 - 根据某标准建立一个比较器来排序
- 根据某标准建立一个比较器来组成堆
1.一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。
public static class Program{
public int start;
public int end;
public Program(int start,int end){
this.start=start;
this.end=end;
}
}
public static class ProgramComparator implements Comparator<Program>{
@Override
public int compare(Program o1,Program o2){
return o1.end-o2.end; //按照会议结束时间升序排列
}
}
//根据会议的结束时间来贪心
public static int BestArranger(Program[] programs,int start){
Arrays.sort(programs, new ProgramComparator());
int result=0;
for(int i=0;i<programs.length;i++){
if(programs[i].start>=start){
result++;
start=programs[i].end;
}
}
return result;
}
2.给定一个字符串类型的数组strs,找到一种拼接方式,使得把所有字符串拼起来之后形成的字符串具有最小的字典序。
public static class MyComparator implements Comparator<String> {
@Override
public int compare(String a, String b) { //a拼b小于b拼a
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;
}
3.一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为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铜板。
输入一个数组,返回分割的最小代价。(其实就是构建一个哈夫曼树)
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;
}
4.给定初始资金,怎么使收益最大
public static class Node{
public int p; //利润
public int c; //花费
public Node(int p,int c){
this.p=p;
this.c=c;
}
}
//按照花费建立小根堆
public static class MinCostComparator implements Comparator<Node>{
@Override
public int compare(Node o1,Node o2){
return o1.c-o2.c;
}
}
//按照利润建立大根堆
public static class MaxProfitComparator implements Comparator<Node>{
@Override
public int compare(Node o1,Node o2){
return o2.p-o1.p;
}
}
public static int findMaximizedCapital(int k,int W,int[] Profits,int[] Capital){
Node[] nodes=new Node[Profits.length];
for(int i=0;i<Profits.length;i++){
nodes[i]=new Node(Profits[i],Capital[i]);
}
PriorityQueue<Node> minCostQ=new PriorityQueue<>(new MinCostComparator());
PriorityQueue<Node> maxProfitQ=new PriorityQueue<>(new MaxProfitComparator());
for(int i=0;i<nodes.length;i++){
minCostQ.add(nodes[i]);
}
for(int i=0;i<k;i++){
while(!minCostQ.isEmpty()&&minCostQ.poll().c<=W){ //解锁能做的项目,加入maxProfitQ
maxProfitQ.add(minCostQ.poll());
}
if(maxProfitQ.isEmpty()){ //如果maxProfitQ里没东西,说明已经没有项目可做了,返回
return W;
}
W+=maxProfitQ.poll().p; //选取能做的项目中利润最高的
}
return W;
}
5.一个数据流中,随时可以取得中位数
思路:
(1)构建一个大根堆,一个小根堆,将第一个数放入大根堆
(2)判断当前数跟大根堆堆顶的数哪个大,若当前数≤大根堆堆顶,当前数入大根堆;否则入小根堆
(3)判断两个堆的大小,若size较大的堆跟size较小的堆的差值为2,则将size较大的堆的堆顶数放入size较小的堆。
public static class MedianHolder {
private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator());
private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator());
private void modifyTwoHeapsSize() {
if (this.maxHeap.size() == this.minHeap.size() + 2) {
this.minHeap.add(this.maxHeap.poll());
}
if (this.minHeap.size() == this.maxHeap.size() + 2) {
this.maxHeap.add(this.minHeap.poll());
}
}
public void addNumber(int num) {
if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.add(num);
} else {
minHeap.add(num);
}
modifyTwoHeapsSize();
}
public Integer getMedian() {
int maxHeapSize = this.maxHeap.size();
int minHeapSize = this.minHeap.size();
if (maxHeapSize + minHeapSize == 0) {
return null;
}
Integer maxHeapHead = this.maxHeap.peek();
Integer minHeapHead = this.minHeap.peek();
if (((maxHeapSize + minHeapSize) & 1) == 0) {
return (maxHeapHead + minHeapHead) / 2;
}
return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead;
}
}
public static class MaxHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
if (o2 > o1) {
return 1;
} else {
return -1;
}
}
}
public static class MinHeapComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
if (o2 < o1) {
return 1;
} else {
return -1;
}
}
}
6.N皇后问题
N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,
也不在同一条斜线上。
给定一个整数n,返回n皇后的摆法有多少种。
n=1,返回1。
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。
n=8,返回92。
public static int num1(int n){
if(n<1){
return 0;
}
int[] record=new int[n]; //record[i]表示第i行的皇后放在了第几列
return process1(0,record,n);
}
//潜台词:record[0...i-1]的皇后,任何两个皇后一定都不共行,不共列,不共斜线
//目前来到了第i行
//record[0...i-1]表示之前行皇后的摆放位置
//n代表整体一共有多少行
//返回值是摆完所有的皇后,合理的摆法有多少种
public static int process1(int i,int[] record,int n){
if(i==n){ //终止行
return 1;
}
int res=0;
for(int j=0;j<n;j++){ //当前行在第i行,j代表第i行的皇后放在第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(i-k)){ //第一个条件判断是否共列,第二个条件判断是否共斜线
return false;
}
}
return true;
}
优化后的解法:
public static int num2(int n) {
if (n < 1 || n > 32) {
return 0;
}
int limit = n == 32 ? -1 : (1 << n) - 1; //若n为9,则新建一个后面9位都是1,前面23位都是0的数
return process2(limit, 0, 0, 0)
}
//colLim 列的限制,1的位置不能放皇后,0的位置可以
//leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以
//rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以
public static int process2(int upperLim, int colLim, int leftDiaLim, int rightDiaLim) {
if (colLim == upperLim) { //base case 表示所有的列都填满了
return 1;
}
int pos = 0;
int mostRightOne = 0;
pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim)); //所有候选皇后的位置,都在pos上,1的位置是能放皇后的
int res = 0;
while (pos != 0) {
mostRightOne = pos & (~pos + 1); //得到候选位置中最右边的那个1
pos = pos - mostRightOne;
res += process2(upperLim, colLim | mostRightOne, //下一个节点在列、左斜线、右斜线上的限制
(leftDiaLim | mostRightOne) << 1,
(rightDiaLim | mostRightOne) >>> 1);
}
return res;
}
七、暴力递归
暴力递归就是尝试:
(1)把问题转化为规模缩小了的同类问题的子问题
(2)有明确的不需要继续进行递归的条件(base case)
(3)有当得到了子问题的结果之后的决策过程
(4)不记录每一个子问题的解
一定要学会怎么去尝试,因为这是动态规划的基础。
1.汉诺塔问题
打印n层汉诺塔从最左边移动到最右边的全部过程
第一步,把i-1个圆盘从左移到中
第二步,把第i个圆盘从左移动右
第三步,把i-1个圆盘从中移到右
public static void hanoi(int n) {
if (n > 0) {
func(n, "left", "right", "mid");
}
}
public static void func(int rest, String from, String to, String help) {
if (rest == 1) {
System.out.println("move 1 from" + from + " to " + to);
} else {
func(rest - 1, from, help, to);
System.out.println("move" + rest + "from" + from + " to " + to);
func(rest - 1, help, to, from);
}
}
2.打印一个字符串的全部子序列,包括空字符串
public static void printAllSubsquence(String str) {
char[] chs = str.toCharArray();
process(chs, 0);
}
//当前来到i位置,要和不要,走两条路
//之前的选择,所形成的结果,是str
public static void process(char[] chs, int i) {
if (i == chs.length) {
System.out.println(String.valueOf(chs));
return;
}
process(chs, i + 1); //要当前字符的
char tmp = chs[i];
chs[i] = 0;
process(chs, i + 1); //不要当前字符的路
chs[i] = tmp;
}
public static void function(String str) {
char[] chs = str.toCharArray();
process(chs, 0, new ArrayList<Character>());
}
//当前来到i位置,要和不要,走两条路
//res之前的选择,所形成的列表
public static void process(char[] chs, int i, List<Character> res) {
if(i == chs.length) {
printList(res);
}
List<Character> resKeep = copyList(res);
resKeep.add(chs[i]);
process(chs, i+1, resKeep); //要当前字符的路
List<Character> resNoInclude = copyList(res);
process(chs, i+1, resNoInclude); //不要当前字符的路
}
3.打印一个字符串的全部排列
public static ArrayList<String> Permutation(String str) {
ArrayList<String> res = new ArrayList<>();
if (str == null || str.length() == 0) {
return res;
}
char[] chs = str.toCharArray();
process(chs, 0, res);
res.sort(null);
return res;
}
//str[i...]范围上,所有的字符,都可以在i位置上,后续都去尝试
//str[0..i-1]范围上,是之前做的选择
//请把所有的字符串形成的全排列,加入到res里去
public static void process(char[] chs, int i, ArrayList<String> res) {
if (i == chs.length) {
res.add(String.valueOf(chs));
}
for (int j = i; j < chs.length; j++) {
swap(chs, i, j);
process(chs, i + 1, res);
swap(chs, i, j);
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
4.打印一个字符串的全部排列,要求不要出现重复的排列
public static ArrayList<String> Permutation(String str) {
ArrayList<String> res = new ArrayList<>();
if (str == null || str.length() == 0) {
return res;
}
char[] chs = str.toCharArray();
process(chs, 0, res);
res.sort(null);
return res;
}
//str[i...]范围上,所有的字符,都可以在i位置上,后续都去尝试
//str[0..i-1]范围上,是之前做的选择
//请把所有的字符串形成的全排列,加入到res里去
public static void process(char[] chs, int i, ArrayList<String> res) {
if (i == chs.length) {
res.add(String.valueOf(chs));
}
boolean[] visit = new boolean[26]; //记录字符是否已被尝试过
for (int j = i; j < chs.length; j++) {
if (!visit[chs[j] - 'a']) {
visit[chs[j] - 'a'] = true;
swap(chs, i, j);
process(chs, i + 1, res);
swap(chs, i, j);
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
5.给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
public static int win1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
}
public static int f(int[] arr, int i, int j) {
if (i == j) {
return arr[i];
}
return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
}
public static int s(int[] arr, int i, int j) {
if (i == j) {
return 0;
}
return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
}
public static int win2(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int[][] f = new int[arr.length][arr.length];
int[][] s = new int[arr.length][arr.length];
for (int j = 0; j < arr.length; j++) {
f[j][j] = arr[j];
for (int i = j - 1; i >= 0; i--) {
f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
}
}
return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
}
6.给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?
public static void reverse(Stack<Integer> stack) { //假设栈中的元素从栈顶到栈底是123
if (stack.isEmpty()) {
return;
}
int i = getAndRemoveLastElement(stack); //则获取元素的顺序是321,当栈为空的时候,最先返回的是1,所以1就被压到栈底了
reverse(stack);
stack.push(i);
}
//用于拿到栈底元素
public static int getAndRemoveLastElement(Stack<Integer> stack) {
int result = stack.pop();
if (stack.isEmpty()) {
return result;
} else {
int last = getAndRemoveLastElement(stack);
stack.push(result);
return last;
}
}
7.数字字符串与字母字符串的转化
public static int number(String str) {
if (str == null || str.length() == 0) {
return 0;
}
return process(str.toCharArray(), 0);
}
//i之前的位置,如何转化已经做过决定了
//i位置之后有多少种转化的结果
public static int process(char[] chs, int i) {
if (i == chs.length) {
return 1;
}
if (chs[i] == '0') {
return 0;
}
if (chs[i] == '1') {
int res = process(chs, i + 1); //i自己作为单独的部分,后续有多少种方法
if (i + 1 < chs.length) {
res += process(chs, i + 2); //i和i+1作为单独的部分,后续有多少种方法
}
return res;
}
if (chs[i] == '2') {
int res = process(chs, i + 1); //i自己作为单独的部分,后续有多少种方法
//i和i+1作为单独的部分并且没有超过26,后续有多少种方法
if (i + 1 < chs.length && (chs[i + 1] >= '0' && chs[i + 1] <= '6')) {
res += process(chs, i + 2);
}
return res;
}
return process(chs, i + 1);
}
8.给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?
public static int maxValue1(int[] weights, int[] values, int bag) {
return process1(weights, values, 0, 0, bag);
}
//i之后的货物自由选择,形成的最大价值返回
//alreadyweight:之前做的决定达到的重量
//重量永远不要超过bag
public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {
if (alreadyweight > bag) {
return 0;
}
if (i == weights.length) {
return 0;
}
return Math.max(
process1(weights, values, i + 1, alreadyweight, bag), //不要i号货物
//要i号货物
values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag));
}
或者
public static int process2(int[] weights,int[] values,int i,int alreadyweight,int alreadyvalue,int bag){
if(alreadyweight>bag){
return 0;
}
if(i==weights.length){
return 0;
}
return Math.max(process2(weights,values,i+1,alreadyweight,alreadyvalue,bag),
process2(weights,values,i+1,alreadyweight+weights[i],alreadyvalue+values[i],bag))
}