代码随想录-算法训练day1-day12

day1(10.30)

数组基础部分

重点:: 数组的元素是不能删的,只能被覆盖。

题目

lc704二分查找

  1. 把握条件

到底是小于 还是小于等于 怎么决定?

关键在于明确区间的定义 到底是[l, r) ** 还是[l ,r ]**

写法一: [ ]

public int search(int[] nums, int target) {
     if (target < nums[0] || target > nums[nums.length - 1]) return -1;
    
    int l = 0, r = nums.length - 1;
    while(l <= r){
        int mid = l + ((r - l)>>1);
        if(nums[mid] == target) return mid;
        else if(nums[mid] > target) r = mid + 1;
        else l = mid - 1;
    }
    return -1;
}

写法二: [ )

public int search(int[] nums, int target) {
     if (target < nums[0] || target > nums[nums.length - 1]) return -1;

    int l = 0, r = nums.length;
    while(l < r){
        int mid = l + ((r - l) >>1);
        if(nums[mid] == target) return mid;
        else if(nums[mid] > target) r = m;
        else l = m + 1;
    }
    return -1;
}

lc27移除元素

使用快慢指针 目的是使用一层for 来干原本两个for的事

fast快指针代表 寻找新数组需要的元素 也就是出了要删除的元素之外的所有其他元素

slow慢指针代表 新数组的下标

这样快指针获取到的值 赋给慢指针就行

当快指针指向的元素不等于要删除的 就是新数组所需要的

public int removeElement(int[] nums, int val) {
    int s = 0;
    for(int f = 0; f < nums.length; f++){
        if(nums[f] != val){
            nums[s] = nums[f];
            s++;
        }
    }
    return s;
}

lc977 有序数组的平方

一个有序数组 也有负数 比如

-5 1 2 3

给他开平方之后返回小到大的排列

因为含有负数 所以开平方之后 最大的元素一定分布在两边

循环条件 只要i <= j的时候就一直继续

public int[] sortedSquares(int[] nums) {

    int l = 0, r = nums.length - 1, k = nums.length - 1;
    wwhile(l <= r){
        if(nums[l] * nums[l] > nums[r] * nums[r]){
            res[k] = nums[l] * nums[l];
            k--;
            l++;
        }else{
            res[k] = nums[r] * nums[r];
            k--;
            r--;
        }
        
    }
    return res;
}

注意:

l 是数组左边界 r 是有边界 j 是结果集数组的下标 设置为原本数组的右边届 往左走<--

左右边界对应的数平方 谁大就取谁

取到左边时候 左边要向右走 指针++

取到右边时候 右边届要想左走 指针--

day2

lc209 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

  • 输入:s = 7, nums = [2,3,1,2,4,3]
  • 输出:2
  • 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

暴力解法:

两个for循环, 不断地找符合条件的子数组,但是时间复杂度是O(n^2) 但是在最后leetcode跟新了数据 这种很明显超时了.

滑动窗口(双指针):

滑动窗口就是不断的调整子序列的起点和终点 直到得出想要的结果

俩指针

i 起点 j 终点 起点和终点最开始都在数组的第一个元素位置

先固定i 移动j 直到找到满足sum 满足了 就停止j终点

此时移动i起点 找到最短满足的 如果出现小于目标值的情况 那么固定i j继续移动

public int minSubArrayLen(int s, int[] nums){

    int l = 0, sum = 0, res = Integer.MAX_VALUE;

    for(int r = 0; r < nums.length; r++){
        sum += nums[r];
        while(sum >= s){
            res = Math.min(res, r - l + 1);
            sum -= nums[l++];
        }
    }
    return res == Integer.MAX_VALUE ? 0 : res;
}

lc59螺旋矩阵I

给你一个正整数 <font style="color:rgba(38, 38, 38, 0.75);background-color:rgb(240, 240, 240);">n</font> ,生成一个包含 <font style="color:rgba(38, 38, 38, 0.75);background-color:rgb(240, 240, 240);">1</font> <font style="color:rgba(38, 38, 38, 0.75);background-color:rgb(240, 240, 240);">n</font><sup><font style="color:rgba(38, 38, 38, 0.75);background-color:rgb(240, 240, 240);">2</font></sup> 所有元素,且元素按顺时针顺序螺旋排列的 <font style="color:rgba(38, 38, 38, 0.75);background-color:rgb(240, 240, 240);">n x n</font> 正方形矩阵 <font style="color:rgba(38, 38, 38, 0.75);background-color:rgb(240, 240, 240);">matrix</font>

本题关键在于边界条件的处理 也就是四方形的四个角怎么处理


遵循一个不变量 到底是[ ] 还是[ )

如果[ ) 也就是在便利一条边时候 只要边的起点 不要边的终点 作为下一条的起点

题目输入n n就是 代表矩阵的大小 比如n = 4 那就返回4*4的矩阵

那么螺旋转圈次数就是n / 2次

如果n是奇数 除以2 除不尽剩余一个

因为每一圈的 起点都不算固定的 所以for循环的i 不可能是固定


 public static int[][] generateMatrix(int n) {
        int[][] arr = new int[n][n];
        int startX = 0, startY = 0, offset = 1,loop = 1,count = 1,x,y;
        while (loop <= n / 2){
            for(y = startY; y < n - offset; y++) arr[startX][y] = count++;
            for(x = startX; x < n - offset; x++) arr[x][y] = count++;
            for( ; y > startY; y--) arr[x][y] = count++;
            for( ; x > startX; x--) arr[x][y] = count++;
            startX++;
            startY++;
            loop++;
            offset++;
        }
        if(n % 2 == 1) arr[startX][startY] = count;
        return arr;
    }

前缀和

题目描述

给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。

输入描述

第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间,直至文件结束。

输出描述

输出每个指定区间内元素的总和。

思路:

  1. 暴力解法

有一个区间之后 直接暴力把这个区间的和都累加一遍 但是提交代码提示超时

  1. 前缀和

前缀和就是利用计算过的子数组的和 从而降低区间查询需要累加计算的次数

在设计计算区间和时候非常有用

比如要计算vec[i] 在这个数组上的区间和

首先累加 p[i]表示下标0~i的vec累加和

计算下标2到5的区间和

区间下标 [2, 5] 的区间和,那么应该是 p[5] - p[1],而不是 p[5] - p[2]。

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] vec = new int[n]; //原本数组
        int[] p = new int[n];  //原本数组的区间和
        // 计算原本数组的区间和
        int preSum = 0;
        for(int i = 0; i < n; i++){
            vec[i] = sc.nextInt();
            preSum += vec[i];
            p[i] = preSum;
        }
        
        while (sc.hasNextInt()){
            int a = sc.nextInt();
            int b = sc.nextInt();
            int sum = 0;
            if(a == 0) sum = p[b];
            else sum = p[b] - p[a - 1];
            System.out.println(sum);
        }

    }
}

开发商买土地

【题目描述】

在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。

现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。

然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。

为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。

注意:区块不可再分。

解法思路:

本题 要求任意两行或者两列之间的数值之和, 也就是前缀和

先计算出行 列方向的和 然后统计好前n行的和 q[n] 如果要求a行到b行之间的总和 那么就是q[b] - q[a - 1]

将一个二维数组划分为两部分,使得这两部分的和之差最小。

首先计算整个数组的总和 sum,然后分别求出每一行和每一列的和,存储在 horizontalvertical 数组中。

接着,通过累加 horizontal 数组中的行和,逐步模拟横向划分,将前几行作为一部分、剩余行作为另一部分。每次划分时,计算当前划分后两部分和的差值,并更新最小差值 result。同理,对于纵向划分,累加 vertical 数组中的列和,模拟不同的列划分方式,计算两部分的差值,并更新最小差值。最终输出的 result 就是所有可能划分方式中两部分和的最小差值。

import java.util.*;

public class Main{
    
    public static void main (String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        int sum = 0;
        int[][] vec = new int[n][m];
        for(int i = 0; i < n; i++){
            for(int j = 0; j < m; j++){
                vec[i][j] = sc.nextInt();
                sum += vec[i][j];
            }
        }
        
        // 统计每行的和
        int[] rowSum = new int[n];
        for(int i = 0; i < n; i++){
            for(int j = 0; j < m; j++){
                rowSum[i] += vec[i][j];
            }
        }
        
        // 统计每列的和
        int[] colSum = new int[m];
        for(int j = 0; j < m; j++){
            for(int i = 0; i < n; i++){
                colSum[j] += vec[i][j];
            }
        }
        
        int res = Integer.MAX_VALUE;
        
        // 计算行划分的最小差值
        int rowCut = 0;
        for(int i = 0; i < n; i++){
            rowCut += rowSum[i];
            res = Math.min(res, Math.abs(sum - 2 * rowCut));
        }
        
        // 计算列划分的最小差值
        int colCut = 0;
        for(int j = 0; j < m; j++){
            colCut += colSum[j];
            res = Math.min(res, Math.abs(sum - 2 * colCut));
        }
        
        System.out.println(res);
        
        sc.close();
    }
}

day3 链表章节

链表基础部分

链表就是通过指针串联在一起的线性结构, 每个节点有两部分组成 分别是数据域 指针域 并且最后一个指针指向null

单链表中的指针只能指向节点的下一个

双链表: 每个节点有两个指针 一个指针指向下一个节点 一个指向上一个节点

循环链表: 首尾相连

定义节点:

public class Node{
    int v;
    Node next;
}

删除节点:

比如a --> b ---> c ----> d ---> null 如果要删除的b节点 那么

只需要将b节点的next指针指向d即可, c节点会被gc回收

添加节点:

在b c中间添加个f 也就是 将b指针指向f f指针指向c

总结:

链表的增加和删除都是O1操作,也不会影响到另一个节点

lc203 移除链表元素

题意:删除链表中等于给定值 val 的所有节点。

示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]

示例 2: 输入:head = [], val = 1 输出:[]

示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]

本题删除链表中的元素

也就是让被删除的前一个节点 指向节点的下一个节点就可以了

但是 如果被删除的节点恰好就是头结点呢? 头节点本身就是第一个节点 不存在前一个节点

所以可以使用虚拟节点法 重新定义一个虚拟临时节点 指向头结点

如果删除的是头结点 那么就可以让临时节点直接指向头结点是下一个节点 最后return 是temp.next

public Node removeElements(Node head, int val) {
    // 1. 设置虚拟节点
    Node temp = new Node();
    temp.next = head;
    // 2. 当前指针
    Node cur = temp;
    // 便利
    while(cur.next != null){
        if(cur.next.val == val) cur.next = cur.next.next;
        else cur = cur.next;
    }
    return temp.next;
}

lc707 设计链表

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

个题涵盖了链表的基本操作

头插 尾插 获取第n个值 第n个节点前插 删除第n个

本题的初始化

class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val) {
        this.val=val;
    }
}
-----
class MyLinkedList {
     //size存储链表元素的个数
    int size;
    //虚拟头结点
    ListNode head;

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    ........
    ........
}
  1. 获取第n个值
public int get(int index){
    if(index < 0 || index >= size) return -1;
    Node cur = head;
    for(int i = 0; i <= index; i++) cur = cur.next;
    return cur.val;
}
  1. 头插
// 头插等价于在第0个元素前添加
public void addAtHead(int val){
    Node newNode = new Node(val);
    newNode.next = head.next;
    head.next = newNode;
    size++;

}
  1. 尾插
// 尾差等价于在(末尾+1)个元素前面添加
public void addAtTail(int val){
    Node newNode = new Node(val);
    Node cur = head;
    while(cur.next != null) cur = cur.next;

    cur.next = newNode;
    size++;

}
  1. 在第n个节点前面插入
// 如果index大于链表的总长度 返回空
// 如果index等于链表的长度 就说明是新插入节点的尾巴节点
// 第index节点之前插入一个新节点  假如index = 0, 那就代表新插入的节点是链表的新头结点
public void addAtIndex(int index, int val){
    if(index > size) return;
    if(index < 0) index = 0;
    size++;

    // 找到要插入节点的前一个节点
    Node pred = head;
    for(int i = 0; i < index; i++) pred = pred.next;
    
    Node toAdd = new Node(val);
    toAdd.next = pred.next;
    pred.next = toAdd;
}
  1. 删除第n个节点
public void delAtIndex(int index){
    if(index < 0 || index >= size) return;
    size--;
    Node pred = head;
    for(int i = 0; i < index; i++) pred = pred.next;
    pred.next = pred.next.next;
}

lc206翻转单链表

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

写法一: 双指针解法

首先 定义了三个节点变量

pre当前节点的前一个节点 初始化为null 因为翻转之后的链表的尾巴节点应该指向null

cur当前节点 初始化为头结点

temp 临时节点 用于暂存cur的下一个节点 避免翻转的时候丢失链表的后续节点

翻转过程

  1. 保存cur.next 到temp
  2. 将cur.next设置为pre 翻转当前节点的指针
  3. 将pre更新为cur 前一个节点就是现在的节点
  4. cur设置为temp 向链表的下一个节点移动
  5. 返回新头结点 pre指向原本链表的最后一个节点 这个节点也就是翻转过后的新头结点
public Node reverseList(Node head){
    Node pre = null, cur = head, temp = null;
    while(cur != null){
        temp = cur.next;
        cur.next = pre;
        pre = cur;
        cur = temp;
    }
    return pre;
}

写法二: 递归

public Node reverseList(Node head){
    return reverse(null, head);
}

private Node reverse(Node pre, Node cur){
    if(cur == null) return pre;
    Node temp = null;
    temp = cur.next;
    cur.next = pre;  // 翻转

    return reverse(cur, temp);
}

day4

lc19 删除倒数第k个节点

本题的关键在于怎么找到倒数第k个节点呢?

使用快慢指针, f快指针先移动k+1步,让f和s相差k个节点, 然后快慢同时走,直到快指针到尾巴,此时慢指针就是倒数第k个节点

public static Node removeNthFromEnd(Node head, int n) {
    // 1. 虚拟头结点 创建双指针
    Node temp = new Node(-1);
    temp.next = head;
    Node f = temp, s = temp;
    // 2. 快指针走k+1
    for(int i = 0; i <= n; i++) f = f.next;
    // 3. f s同时走
    while(f != null){
        f = f.next;
        s = s.next;
    }
    // 4. 删除s指向 的节点 并返回
    if(s.next != null) s.next = s.next.next;
    return temp.next

}

链表相交

本题关键在于:

**设置双指针 当两个指针遍历完自己的链表之后,在切换到对方的链表继续遍历。只要相遇,那一定是在交点相遇。如果没有交点,那就在 **null** 相遇。
**

public Node getIntersectionNode(Node headA,Node headB){
    Node indexA = headA, indexB = headB;
    while(indexA != indexB){
        if(indexA == null) indexA = headB;
        else indexA = indexA.next;
        if(indexB == null) indexB = headA;
        else indexB = indexB.next;
    }
    return indexA;

}

lc142. 环形链表II

本题包含两部分

  1. 判断是否有环

通过快慢指针 但凡相遇 那一定有环

  1. 找到环的入口

有环之后 定义两个指针 一个指向快或者慢指针 一个指向头结点 开始同步移动 当两个相等相遇的地方 一定是环的入口

public Node detectCycle(Node head) {
    Node f = head, s = head;
    while(f != null && f.next != null){
        f = f.next.next;
        s = s.next;
        if(f == s){
            Node index1 = f, index2 = head;
            while(index1 != index2){
                index1 = index1.next;
                index2 = index2.next;
            }
            return index1;
        }
    }	

    return null;
}

lc24 两两交换链表中的节点

本题依旧采用虚拟头结点方式

修改这个节点的指向 必须找到这个节点的前一个节点指针

改变指针的走向实现交换 并不是交换数值

  1. 虚拟头结点

  2. 定义 cur 变量并初始化为 dumpHead,用于遍历链表,控制节点交换。

定义辅助变量 :

temp: 临时节点,保存两个节点后面的节点

firstNode、保存两个节点之中的第一个节点

secondNode,保存两个节点之中的第二个节点

  1. while循环 条件是是 cur.nextcur.next.next 不为 null

其中cur.nextcur.next.next 代表一对要交换的节点,如果不足一对(即只剩下一个节点或没有节点),则循环结束。

  1. 交换

temp 保存第二个节点的下一个节点,

firstNode 指向当前要交换的第一个节点,secondNode 指向当前要交换的第二个节点。

开始交换.....

  • cur.next = secondNode:将 cur 的下一个节点指向 secondNode,完成第一步交换。
  • secondNode.next = firstNode:将 secondNode 的下一个节点指向 firstNode,完成第二步交换。
  • firstNode.next = temp:将 firstNode 的下一个节点指向 temp(即下一对的第一个节点)。
  • cur = firstNode:将 cur 移动到 firstNode,准备开始下一轮交换。
public Node swapPairs(Node head) {
    Node dumpHead = new Node(-1);
    dumpHead.next = head;
    Node cur = dumpHead, temp, firstNode, secondNode;
    while(cur.next != null && cur.next.next != null){
        temp = cur.next.next.next;
        firstNode = cur.next;
        secondNode = cur.next.next;

        cur.next = secondNode;
        secondNode.next = firstNode
        firstNode.next = temp;
        cur = firstNode;

    }
    return dumpHead.next;
}

day06哈希表

总结的来说,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

lc242有效的字母异位词

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

示例 1: 输入: s = "anagram", t = "nagaram" 输出: true

示例 2: 输入: s = "rat", t = "car" 输出: false

说明: 你可以假设字符串只包含小写字母。

读题可以看不懂到底什么是字母异位词呢?

大白话就是 给定两个字符串 判断是否有相同的字母组成(不管顺序)

比如 abbc --- bbac 这这就是一对字母异位词

因为知道这些字母只能是小写字母 而且字母的asc码都是连续的

所以可以定一个数组 大小是26 刚好能放入26个字母 用来记录字符串s里字符出现的次数。

因为asc码都是连续的 所以a到z是26个连续的数 所以将(s[i] - 'a')++ 求出来一个数字

同样,在便利t的时候,只需要-- 就可以了

其实也就是26个字母对应26个下标 出现一次就对应下标加一 出现的位置比如

t='abcdef.....z' 26个 那么依次加加操作之后就是 arr[1,1,1,1,1,1,1,1,1,1,1,1,......1] 26个1

最后检查这个数组,如果有元素不等于0 说明s或者t字符串一定有一个多了 或者少了

如果都是等于0 说明就是字母异位词

时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。

--------

也就是:

第一次便利s的时候 遇到每个字母就把他在数组对应的位置加1 表示出现过

第二次便利t的时候 遇到的时候 就把对应位置减1

如果最后结果数组都是0 代表两个字符串完全相等

public boolean isAnagram(String s, String t) {
    int[] arr = new int[26];

    for(int i = 0; i < s.length(); i++) arr[s.charAt(i) - 'a']++;

    for(int i = 0; i < t.length(); i++) arr[t.chatAt(i) - 'a']--;

    for(int c : arr){
        if(c != 0) return false;
    }
    return true;
}

lc349两个数组的交集

> 题意:给定两个数组,编写一个函数来计算它们的交集。 > > 输入:nums1 = [4,9,5],nums2 = [9,4,9,8,4] > > 输出:[9,4] >

使用set结构 定义 s1 res

s1 用于存放num1中的元素也就是 s1 = {4,9,5}

res 用于nums1和nums2经过去重之后交集部分

执行结果:

  • nums2[0] = 9s1 包含 9,添加到 resres = {9}
  • nums2[1] = 4s1 包含 4,添加到 resres = {9, 4}
  • nums2[2] = 9res 已经包含 9,所以不重复添加。
  • nums2[3] = 8s1 不包含 8,跳过。
  • nums2[4] = 4res 已经包含 4,所以不重复添加。

最终,res = {9, 4},这是 nums1nums2 的交集。

public int[] intersection(int[] nums1, int[] nums2) {
    if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) return new int[0];

    // 初始化set
    Set<Integer> s1 = new HashSet<>();
    Set<Integer> res = new HashSet<>();

    //便利num1
    for(int i : nums1) s1.add(i);

    // 便利num2时候需要先去重
    for(int i : nums2){
        if(s1.contains(i)) res.add(i);
    }

    //申请新数组存放结果 并且返回
    int[] arr = new int[res.size()];
    int index = 0;
    for(int num : res) arr[index++] = num;
    return arr;
}

lc202 快乐数

「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。

编写一个算法来判断一个数 <font style="color:rgba(38, 38, 38, 0.75);background-color:rgb(240, 240, 240);">n</font> 是不是快乐数。

题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现

这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。

public boolean isHappy(int n) {
    Set<Integer> set = new HashSet<>();
    while(n != 1 && set.contains(n)){
        set.add(n);
        n = getNumber(n);

    }
    return n == 1;

}
public int getNumber(int n) {
    int res = 0;
    while(n > 0){
        int temp = n % 10 //取出个位
        res += temp * temp;
        n /= 10; 

    }
    return res;
}


lc1 两数之和

梦的开始!!!!!

使用map来记录数字和下标

map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。

public int[] twoSum(int[] nums. int target){
    int[] res = new int[2];
    if(nums == null || nums.length ==0) return res;

    Map<Integer, Integer> map = new HashMap<>();
    for(int i = 0; i < nums.length; i++){
        int temp = target - nums[i];
        if(map.containsKey(temp)){
            res[1] = i;
            res[0] = map.get(temp);
            break;

        }
        map.put(nums[i], i);

    }
    return res;


}

day7

lc383 救赎信

和 242.有效的字母异位词 是一个思路 ,算是拓展题

lc383是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a

通过题意得出 1. magazines里面元素不能重复使用 2. 只有小写


   public boolean canConstruct(String ransomNote, String magazine){
        if(ransomNote.length() > magazine.length()) return false;
        
        int[] res = new int[26];
        
        for(char c : magazine.toCharArray()) res[c - 'a']++;
        
        for(char c : ransomNote.toCharArray()) res[c - 'a']--;
        
        for(int i : res){
            if(i < 0) return false;
        }
        return true;
    }

lc454 四数相加II

给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,� 请你计算有多少个元组 (i, j, k, l) 能满足:�

  1. 0 <= i, j, k, l < n�
  2. nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0�

给你4个数组 在这4个数组分别找出一个元素 使得这几个元素相加等于0 问你一共多少对符合条件的(不要求分别是多少 只要有多少个符合要求)


因为是在4个数组里面 分别找一个 所以不用考虑去重

解法一: 暴力

暴力法: 4个for循环去便利4个数组 然后相加等于0的话 就++; 最后返回cout

但是时间复杂度是n的4次方

解法二: 使用HashMap 不仅要统计元素 还要统计次数

把4个数组分成两组,前两个数组 元素为a+b 放入map的key 同时统计a+b出现的次数

然后便利cd数组时候 去判断0-(c+d)有没有出现过 统计出现过的次数 然后count加value的值


为什么分成两组?

因为两个for循环时间复杂度是n的平方 另外一组的两个for也是n的平方 所以整体时间复杂度就是n的平方 因为时间复杂度是不管常数项的


为什么是 0-(c+d)?

如果0-(c+d) = a + b ----> 那么就是 a+b+c+d = 0


  public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        Map<Integer, Integer> map = new HashMap<>();
        int res = 0;
        // 统计前两个数组中的元素之和并且统计次数
        // 如果sum存在map  返回 对应值(出现次数) 不在就默认返回0
        for(int i : nums1){
            for(int j : nums2){
                int sum = i + j;
                map.put(sum, map.getOrDefault(sum, 0) + 1);
            }
        }
        // 统计后俩
        for(int i : nums3){
            for (int j : nums4){
                res += map.getOrDefault(0 - i - j, 0);
            }
        }
        return res;
    }

lc15 三数之和

** **给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。


注意 : 一个数组里面 去重

因为需要对ABC都要进行去重操作 所以属于双指针更加方便

先进行排序 然后三个指针 i l r i定于在数组开头 l在i+1位置 r在尾巴

也就转换成arr[i]+arr[l]+arr[r] = 0 接下来就是移动指针的操作了

如果三个相加大于0 那么l向后移动 直到l和r相遇

但是要对abc去重 也就是arr[i] arr[l] arr[r] 进行去重操作

如果a重复了 因为所以应该跳过去

还有一个问题就是判断arr[i] 和 arr[i + 1] 还是判断 arr[i - 1] 和 arr[i]呢?

如果是判断i+1进行去重的话 就把三元组结果集里面的重复数据去除了 比如{-1,-1,2}

我们要做的是 不能有重复的三元组,但结果集内的元素是可以重复的!

if (i > 0 && nums[i] == nums[i - 1])  continue;

那么b和c怎么去重呢?

其实这个去重应该放在找到一个结果集之后再进行去重

while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;

整体代码

public List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();

    for (int i = 0; i < nums.length; i++) {
        if (nums[i] > 0) break; // 如果当前数大于0,则直接结束循环
        if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重

        int l = i + 1, r = nums.length - 1; // 每次for循环都重新初始化l和r
        while (l < r) {
            int sum = nums[i] + nums[l] + nums[r];
            if (sum > 0) r--;
            else if (sum < 0) l++;
            else {
                res.add(Arrays.asList(nums[i], nums[l], nums[r]));

                // 去重处理
                while (l < r && nums[r] == nums[r - 1]) r--;
                while (l < r && nums[l] == nums[l + 1]) l++;
                r--;
                l++;
            }
        }
    }
    return res;
}
    

lc18 四数之和

思路和三数之和差不多 就是多套了一层for

三数之和是三个指针 那么四数就是4个指针 关键点是target可以是负数 两个负数相加是可以变得更小的 所以在去重的时候要加大于0的条件

四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。

public List<List<Integer>> fourSum(int[] nums, int target) {
    // 排序数组
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();
    for(int k = 0; k < nums.length; k++){
        // 剪枝
        if(nums[k] > target && nums[k] > 0) break;
        //去重
        if(k > 0 && nums[k] == nums[k - 1]) continue;

        for(int i = k + 1; i < nums.length; i++){
            // 剪枝 去重i
            if(nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) break;
            if(i > k + 1 && nums[i] == nums[i - 1]) continue;
            // l 和 r
            int l = i + 1, r = nums.length - 1;
            while (l < r){
                long sum = nums[k] + nums[i] + nums[l] + nums[r];
                if(sum > target) r--;
                else if (sum < target) l++;
                else {
                    res.add(Arrays.asList(nums[k],nums[i],nums[l],nums[r]));
                    // l 和 r去重
                    while (l < r && nums[r] == nums[r - 1]) r--;
                    while (l < r && nums[l] == nums[l + 1]) l++;
                    r--;
                    l++;
                }
            }
        }

    }
    return res;
}

day08--字符串篇章

lc344 翻转字符串

本题使用双指针 一个卡在开头 一个卡在结尾 ,同时向中间移动 然后交换两个指针指向的数

其中交换数可以使用第三个变量temp 也可以使用位运算不借助任何其他

原理如下:

假设

int a = 13,b = 14

a = a ^ b;

b = a ^ b;

a = a ^ b;


证明: 假设 a是甲,b是乙

a = 甲 ^ 乙

b = 甲 ^ 乙 ^ 乙 = 甲

a = 甲 ^ 乙 = 甲 ^ 乙 ^ 甲 = 乙


注意: a,b两个数必须不同位置,不同地址

本题注意 如果要携程
s[l] = s[l] ^ s[r]; s[r] = s[l] ^ s[r]; s[l] = s[l] ^ s[r];

需要进行类型转换 (char)

s[l] = (char) (s[l] ^ s[r]); s[r] = (char) (s[l] ^ s[r]); s[l] = (char) (s[l] ^ s[r]);

而使用^= 会自动进行类型转换

public void reverseString(char[] s) {
    int l = 0, r = s.length - 1;
    while(l < r){
        s[l] ^= s[r];
        s[r] ^= s[l];
        s[l] ^= s[r];
        l++;
        r--;
    }

}

解法二: 队列

因为队列是先进后出 入栈之后 再取出自动完成倒序了 但是缺点是消耗内存

public void reverseString(char[] s) {
    Stack<Character> stack = new Stack<>();
    
    // 将所有字符压入栈中
    for (char c : s) stack.push(c);
    
    // 将字符从栈中弹出,依次放回数组
    int i = 0;
    while (!stack.isEmpty()) s[i++] = stack.pop();
}


lc541翻转字符串II

给定一个字符串 s 和一个整数 k,从字符串开头算起,�每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。�

剩余字符少于 k 个,则将剩余字符全部反转。�

如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。�

题目意思是 假设字符串是"12345678" k= 3

其中2k也就是6 翻转前k也就是翻转1-3 剩余78 不够k 就全部翻转

变成"32145687"

因为每次都是2k 所以for循环时候 直接i+=2k

public String reverseStr(String s, int k) {
    char[] ch = s.toCharArray();
    // 1. 每隔2k翻转前k
    for(int i = 0; i < ch.length; i += 2 * k){
        // 2. 剩余 >=x并且<2k 那就翻转前k
        if(i + k <= ch.length){
            reverse(ch, i, i + k - 1);
            continue;
        }
        //3. 剩余的<k 那就把剩余的全部翻转
        reverse(ch, i, ch.length - 1);
    }
    return new String(ch);

}
public void reverse(char[] ch, int i, int j) {
    while (i < j){
        ch[i] ^= ch[j];
        ch[j] ^= ch[i];
        ch[i] ^= ch[j];
        i++;
        j--;
    }
}

替换数字

给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。

例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"。

第一步 肯定要先把数组扩容到能够容纳转换后的大小

比如原本 "a5b" 的长度为3 那么转换后就是"anumberb" 长度为 8。

然后使用双指针 从后向前替换掉数字 i指针指向新长度的尾巴 j指向旧长度的尾巴

从旧数组 从后向前 开始赋值到新数组 新数组也是后向前便利将s[j]赋值给s[i]

因为从后往前就可以避免元素的移动

本题使用oj模式

// 先把原数组复制到扩展长度后的新数组,然后不再使用原数组、原地对新数组进行操作。
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String s = sc.next();
        int len = s.length();
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) >= 0 && s.charAt(i) <= '9') {
                len += 5;
            }
        }
        
        char[] ret = new char[len];
        for (int i = 0; i < s.length(); i++) {
            ret[i] = s.charAt(i);
        }
        for (int i = s.length() - 1, j = len - 1; i >= 0; i--) {
            if ('0' <= ret[i] && ret[i] <= '9') {
                ret[j--] = 'r';
                ret[j--] = 'e';
                ret[j--] = 'b';
                ret[j--] = 'm';
                ret[j--] = 'u';
                ret[j--] = 'n';
            } else {
                ret[j--] = ret[i];
            }
        }
        System.out.println(ret);
    }
}

day9

lc151 翻转字符串的单词

给定一个字符串,逐个翻转字符串中的每个单词。
示例 1:
输入: "the sky is blue"
输出: "blue is sky the"

注意: 1. 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

  1. 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

从题目提取的信息: 1. 依照单词为单位进行翻转(字母顺序不能变)

  1. 结果的前后不能有空格
  2. 单词之间如果有多个空格 只保留一个

翻转思路: "the sky is blue"

  1. 先不管字母顺序 整体进行翻转 保证单词的位置一致---变成 "eulb si yks eht"
  2. 再翻转单词 确保单词顺序 "blue is sky the"

所以整体思路就是

  1. 移除多余的空格
  2. 先全部翻转句子
  3. 翻转单词
class Solution {
    public String reverseWords(String s) {
        char[] chars = s.toCharArray(); // 先转成字符数组
        chars = delSpaces(chars);   // 1. 处理空格
        reverse(chars, 0, chars.length - 1);  // 2.翻转句子
        reverseEasyWord(chars);  // 3. 翻转单词
        return new String(chars);
    }
    public char[] delSpaces(char[] c){
        int s = 0;
        for(int f = 0; f < c.length; f++){
            if(c[f] != ' ') {
                if(s != 0) c[s++] = ' ';
                while(f < c.length && c[f] != ' ') c[s++] = c[f++];
            }
        }
        char[] newChar = new char[s];
        System.arraycopy(c, 0, newChar, 0, s);
        return newChar;
    }
    public void reverse(char[] c, int l , int r){
        if(c.length <= r) return;
        while(l < r){
            c[l] ^= c[r];
            c[r] ^= c[l];
            c[l] ^= c[r];
            l++;
            r--;
        }
    }
    public void reverseEasyWord(char[] c){
        int start = 0;
        for(int end = 0; end <= c.length; end++){
            if(end == c.length || c[end] == ' '){
                reverse(c, start, end - 1);
                start = end + 1;
            }
        }
    }

}

右旋字符串�

字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。

对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。

思路:

把字符串分成两部分 第一段是字符串的长度-n 第二段长度是n 也就是转换成 把第二段放在开头 把第一段放在后面

先不考虑整体顺序 把整体进行翻转 然后再来一次翻转

import java.util.*;

public class Main{
    
    public static void main (String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = Integer.parseInt(sc.nextLine());
        
        String s = sc.nextLine();
    
        int len = s.length();
        char[] c = s.toCharArray();
        // 翻转整体
        reverString(c, 0, len - 1)
        // 原本的第二段变成了第一段 所以开始是0  结束是n-1
        reverString(c, 0, n - 1);
        // 翻转原本的第一段
        reverString(c, n, len - 1);
        System.out.println(c);
        
        
    }
    public static void reverString(char[] ch, int l ,int r){
        while (l < r){
            ch[l] ^= ch[r];
            ch[r] ^= ch[l];
            ch[l] ^= ch[r];
            l++;
            r--;
        } 
    }
}

KMP算法基础

理论基础

当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

假设 文本串是"aabaabaaf" 目标是"aabaaf" 求文本串是否出现了目标串

字符串1 aabaabaaf

字符串2 aabaaf

kmp算法就是字符串2 在f不匹配的位置 他会跳到b也就是之前匹配过的内容

那么如何找到b呢 就是使用**前缀表 **找到不匹配f的前面是aa 那么就找到aa相等的前缀的后面 重新开始匹配

什么是前缀什么是后缀? aabaaf

前缀是包含首字母 不包含尾巴的所有子串 也就是 a aa aab aaba aabaa

后缀是包含尾巴 不包含开头的所有子串 f af aaf baaf abaaf

最长相等前后缀

a 因为即是开头又是结尾 所以是没有前后缀

aa 前缀a 后缀a 所以1

aab 因为开头a结尾b 所以找不到相等 是0

aaba 前缀是aab 后缀aba 相等的是a 所以长度是1

aabaa 前缀aaba 后缀是abaa 相同是aa 长度为2

aabaaf 前缀aabaa 后缀是abaaf 因为f在前缀中不存在 所以是0

所以aabaaf的前缀表就是[0 1 0 1 2 0] 那么是怎么匹配的呢

文本串 a a b a a b a a f

模式串 a a b a a f

前缀表 0 1 0 1 2 0

遇到f不匹配了 找前面最长相等前后缀 也就是2 也就意味着 有个后缀aa 前面也有个相同的aa

然后跳到前面相等的前缀后面 也就是aa的后面 就是b


在kmp算法中都会提到next数组

是遇到冲突的地方之后next数组会告诉我们回退到哪里

会把前缀表做个右移的操作 或者统一减一的操作

代码实现

在代码实现中分为下面步骤

  1. 初始化 next数组 各个变量 2. 处理前后缀不相同的情况 3. 相同的情况 4. 更新next

这个2也就意味着需要跳到下标为2的元素上继续匹

  1. 初始化

j是指向前缀末尾位置 初始化为0

i是指向后缀末尾位置 在for里面1开始

  1. 前后缀不相同情况

也就s[i] != s[j] 但是要保证j >0 j向前回退 看前一位的前缀表对应下标 j = next[j - 1]

  1. 前后缀相同的情况

也就是 if(s[i] == s[j]) j++;

  1. 更新next

next[i] = j


day10--栈and队列

栈: 先进后出 像弹夹 子弹压堂

队列: 先进先出 像管道 双端都有口

Lc232 用栈实现队列

使用栈实现队列的下列操作

push(x) -- 将一个元素放入队列的尾部。
pop() -- 从队列首部移除元素。
peek() -- 返回队列首部的元素。
empty() -- 返回队列是否为空。

public class Lc232_myQueue {
    Stack<Integer> sIn, sOut;

    public Lc232_myQueue() {
        sIn = new Stack<>();  //进栈
        sOut = new Stack<>(); //出栈

    }

    public void push(int x) {
        sIn.push(x);
    }
    public int pop() {
        dumpStackIn();
        return sOut.pop();

    }

    public int peek() {
        dumpStackIn();
        return sOut.peek();

    }

    public boolean empty() {
        return sIn.isEmpty() && sOut.isEmpty();
    }
//    如果出栈为空 那么就将In中的元素全部放入Out里面
    private void dumpStackIn() {
        if(!sOut.isEmpty()) return;
        while (!sIn.isEmpty()) sOut.push(sIn.pop());
    }
}

Lc225. 用队列实现栈

使用队列实现栈的下列操作:

  • push(x) -- 元素 x 入栈
  • pop() -- 移除栈顶元素
  • top() -- 获取栈顶元素
  • empty() -- 返回栈是否为空

一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。


class Lc225_myStack {
    Queue<Integer> q;

    public Lc225_myStack() {
        q = new ArrayDeque<>();
    }

    public void push(int x) {
        q.add(x);
    }

    public int pop() {
        rePosistion();
        return q.poll();

    }
    public int top() {
        rePosistion();
        int res = q.poll();
        q.add(res);
        return res;
    }

    public boolean empty() {
        return q.isEmpty();
    }
    private void rePosistion() {
        int size = q.size();
        size--;
        //只要 size 仍然大于 0,就继续循环,同时在每次循环中 size 会减 1。
        while (size-- > 0) q.add(q.poll());
    }

}

Lc20有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。�

其实总的来说不匹配的情况一共有三种

  1. 左边( 多余

在便利字符串的时候 遇到了左的({[ 就把对应的右括号放入栈内

如果便利的时候遇到了右边的 就弹出对应栈内的括号

如果字符串遍历完了 但是栈不为空 说明 不匹配 多了

  1. (} 类型不匹配

遇到相同方向的 但是对比发现类型不一样

  1. 右边多了

字符串还没遍历完 栈就空了

public boolean isValid(String s) {
    // 如果长度是奇数 那一定不满足要求
    if(s.length() % 2 != 0) return false;

    Deque<Character> deque = new LinkedList<>();
    char ch;
    for(int i = 0; i < s.length(); i++){
        ch = s.charAt(i);
        if(ch == '(') deque.push(')');
        else if (ch == '{') deque.push('}');
        else if(ch == '[') deque.push(']');
        // 情况23
        else if (deque.isEmpty() || deque.peek() != ch) return false;
        else deque.pop();
    }
    // 情况1 字符串便利完了 但是栈不为空
    return deque.isEmpty();


}

Lc1047. 删除字符串中的所有相邻重复项�

本题和上题的匹配括号类似, 本题是匹配相邻元素,最后都是做消除的操作

那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。 然后再去做对应的消除操作

class Solution {
    public String removeDuplicates(String s) {
        ArrayDeque<Character> q = new ArrayDeque<>();
        char ch;
        for(int i = 0; i < s.length(); i++){
            ch = s.charAt(i);
            if(q.isEmpty() || q.peek() != ch) q.push(ch);
            else q.pop();
        }
        String res = "";
        while(!q.isEmpty()) res = q.pop() + res;
        return res;
        
    }
}

day11

Lc150. 逆波兰表达式求值

逆波兰表达式其实就是后缀表达式 是指运算符写在后面。 使用栈来求解

遇到数字就放入栈内 遇到操作符就****取出栈顶两个数字进行计算,并将结果压入栈中

注意:

在本题要注意: 减法和除法 是对前后两个顺序有要求 后弹出的减去 或者除以先弹出的. �因为在栈中,后进先出会导致先压入的元素在减法时应该作为减数,后压入的元素应作为被减数。 所以 减法的写法是 -d.pop() + d.pop() 如果不想这样写 就换成除法一样 两个int

public int evalRPN(String[] tokens) {
    Deque<Integer> d = new LinkedList<>();
    for(String s : tokens){
        if("+".equals(s)) d.push(d.pop() + d.pop());
            
        else if("-".equals(s)) d.push(-d.pop() + d.pop());
            
        else if("*".equals(s)) d.push(d.pop() * d.pop());
        else if("/".equals(s)) {
            int n1 = d.pop();
            int n2 = d.pop();
            d.push(n2 / n1);
        }else d.push(Integer.valueOf(s));

    }
    return d.pop();


}

LC239.滑动窗口最大值

使用单调队列 也就是维护队列里面单调递增或者递减

day12-二叉树

基础概念

树:

  • 树的度:树内各个节点的最大值, 比如节点a 有3个节点,那度就是3
  • 叶子节点(终端节点):他的度为0,比如i j
  • **孩子、双亲: **比如a节点,他有3个子树t1,t2,t3 那这三个子树的根节点是bcd,那a节点的孩子就是bcd。bcd的双亲就是a
  • 兄弟节点: 如果节点有共同的双亲,那他们就是兄弟节点,比如hij的双亲都是d,那hij是兄弟节点
  • 树的层: 横着来 有多少层
  • 堂兄弟: 他们的双亲位于同一层
  • 节点的祖先: 从根到该节点所经过分支上的所有节点,比如a到m需要经过a_d_h。那adh都是m的祖先
  • 节点的子孙:以某节点为跟的子树中的任意节点都是该节点的子孙
  • **树的深度(高度): 树中节点的最大层次 **

**满二叉树: **

如果一颗二叉树只有度为0的节点和 度为2的节点 并且度为0的节点在同一层上, 就是满二叉树

这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。

完全二叉树::

除了最底层节点可能没有填满之外,其余的每一层的节点都达到了最大值,并且最下面一层的节点都几种在该层的最左边的若干位置, 如果最底层是h层 那么该层包含1~2^(h-1)个节点


大白话就是 左边节点/全是满的 不能缺 一旦缺少左边节点就不是了

平衡二叉树::(AVL)

一颗空树 或者他的左右两个子树的高度差的绝对值不能超过1 并且左右两个子树都是AVL

最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。


二叉树的存储方式

链式存储(指针):通过指针把分布在各个地址的节点串联一起。

顺序存储(数组):

如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。 但一般常用链式存储


二叉树的遍历


一. 深度优先遍历(看头在哪 也就是中间节点)

  1. 先序遍历: 头 -> 左 -> 右
  2. 中序遍历: 左 -> 头 -> 右
  3. 后序遍历: 左 -> 右 -> 头

二. 广度优先遍历

也就是层次遍历, 使用队列先进先出的特点 因为先进先出所以才能一层一层的便利


Java中二叉树定义

public class Node{
    int v;
    Node l;
    Node r;
    Node(){
        
    }
    Node(int v){
        this.v = v;
    }
    Node(int v, Node l, Node r){
        this.v = v;
        this.l = l;
        this.r = r;
    }
    
}

二叉树遍历代码(递归)

节点的定义 如上, 下面代码不再定义节点

// 调用递归方法
public List<Integer> preorderTraversal(Node root) {
    List<Integer> res = new ArrayList<>();
    prePrint(root, res);
    // midPrint(root, res);
    // postPrint(root, res);

    return res;
}
// lc144---前序便利 跟左右
public void prePrint(Node root, List<Integer> res) {
    if(root == null) return;
    res.add(root.v);
    prePrint(root.l, res);
    prePrint(root.r, res);
}
// lc94----中序遍历  左跟右
public void midPrint(Node root, List<Integer> res) {
    if(root == null) return;
    midPrint(root.l, res);
    res.add(root.v);
    midPrint(root.r, res);
}
// lc145---- 后续便利  左右跟
public void postPrint(Node root, List<Integer> res) {
    if(root == null) return;
    postPrint(root.l, res);
    postPrint(root.r, res);
    res.add(root.v);
}

朋友们,有没有发现一个规律呢?

就是主要看add行为在哪里 add在前面就是根左右--前序 中间就是左跟右--中序 最后就是左右跟-后续

为什么能由一个递归函数来实现呢? 递归序

二叉树便利(迭代法)

使用栈, 压栈, 因为栈是先进后出 注意进出的顺序

使用栈前序便利

思路:

  1. 创建个Stack栈 并且放入头结点
  2. while便利 不为空时候 立马pop出头
  3. 先压入右 再压入左
// lc144---前序便利 跟左右
public  List<Integer> prePrint(Node root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return res;
    Stack<Node> s = new Stack<>();
    s.push(root);
    while(!s.isEmpty()){
        root = s.pop();
        res.add(root.v);
        if(root.r != null) s.push(root.r);
        if(root.l != null) s.push(root.l);
    }
        
    return res;
   
}

后遍历

**在前序便利是跟左右 入栈是先右再左. 那么后序遍历左右跟 改成 入栈 是跟右左 然后再把数组做一个翻转 就变成了左右跟 **

只需要在前序的基础上 入栈顺序改成先左再右 然后再使用库函数做一个翻转


public  List<Integer> postPrint(Node root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return res;
    Stack<Node> s = new Stack<>();
    s.push(root);
    while(!s.isEmpty()){
        root = s.pop();
        res.add(root.v);
        if(root.l != null) s.push(root.l);
        if(root.r != null) s.push(root.r);

    }
    Collections.reverse(res);
    return res;


}

中序遍历


中序遍历是 左跟右 在入栈时候关键在于先一直将左子节点压入栈,直到到达左子树的尽头,然后开始出栈访问节点,最后处理右子节点。

public List<Integer> midPrint(Node root){
    List<Integer> res = new ArrayList<>();
    if(root == null) return res;
    Stack<Node> s = new Stack<>();
    Node cur = root;
    while (cur != null && !s.isEmpty()){
        // 1. 遍历左子节点,将当前节点压入栈,并将 cur 移动到左子节点
        if(cur != null){
            s.push(cur);
            cur = cur.l;
        }else {
            // 2. 如果当前节点为空(说明已经到达左子树的尽头)
            // 从栈中弹出节点,访问该节点并将其值加入结果列表
            cur = s.pop();
            res.add(cur.v);
            // 3. 然后将 cur 移动到右子节点,继续对右子树进行相同操作
            cur = cur.r;
        }
    }
    return res;
}

二叉树前中后统一模版

在上面中序遍历中, 提到使用栈无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。

就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

//      先序遍历
public List<Integer> prePrint(Node root) {
    List<Integer> res = new ArrayList<>();
    Stack<Node> s = new Stack<>();
    if (root != null) s.push(root);
    while (!s.isEmpty()){
        Node cur = s.peek();
        if(cur != null){
            s.pop();
            if(cur.r != null) s.push(cur.r);
            if(cur.l != null) s.push(cur.l);
            s.push(cur);
            s.push(null);
        }else{
            s.pop();
            cur = s.peek();
            s.pop();
            res.add(cur.v);
        }
    }
    return res;
}

// 后
public List<Integer> postPrint(Node root){
    List<Integer> res = new ArrayList<>();
    Stack<Node> s = new Stack<>();
    if(root != null) s.push(root);
    while (!s.isEmpty()){
        Node cur = s.peek();
        if(cur != null){
            s.pop();
            s.push(cur);
            s.push(null);
            if(cur.l != null) s.push(cur.l);
            if(cur.r != null) s.push(cur.r);
        }else{
            s.pop();
            cur = s.peek();
            s.pop();
            res.add(cur.v);
        }
    }
    return res;
}

/**
 * 中序遍历
 * @param root
 * @return
 */
public List<Integer> midPrint(Node root){
    List<Integer> res = new ArrayList<>();
    Stack<Node> s = new Stack<>();
    if (root != null) s.push(root);
    while (!s.isEmpty()){
        Node cur = s.peek();
        if(cur != null){
            s.pop();
            if(cur.r != null) s.push(cur.r);
            s.push(cur);
            s.push(null);
            if (cur.l != null) s.push(cur.l);
        }else {
            s.pop();
            cur = s.peek();
            s.pop();
            res.add(cur.v);
        }
    }

    return res;
}

仔细观察 重点在if(cur != null)上面 else等于空的情况是一样的 所以可以抽象出一个模版框架

public List<Integer> xxxPrint(Node root){
    List<Integer> res = new ArrayList<>();
    Stack<Node> s = new Stack<>();
    if(root != null) s.push(root);
    while(!s.isEmpty()){
        Node cur = s.peek();
        if(cur != null){
            s.pop();
            // ....
            // ....
            // ....
            // ....
            

        }else{
            s.pop();
            cur = s.peek();
            s.pop();
            res.add(cur.v);
        }

    }
    return res;

}

那么代码里面的不等于空的时候怎么写呢?

通过调整节点入栈顺序null标记的位置,可以实现不同的遍历顺序:

  • 前序遍历:右r - 左l - 根 + null

因为前序便利是跟左右 那么入栈就是 右左跟 再来push一个null

if(cur.r != null) s.push(cue.r);

if(cur.l != null) s.push(cue.l);

s.push(cur);

s.push(null);

  • 中序遍历:右 - 根 + null - 左

中序遍历是左跟右 入栈就是右 跟 null 左

if(cur.r != null) s.push(cue.r);

s.push(cur);

s.push(null);

if(cur.l != null) s.push(cur.l);

  • 后序遍历:根 + null - 右 - 左

后续便利是左右跟 入栈是 跟 null 右 左

s.push(cur);

s.push(null);

if(cur.r != null) s.push(cue.r);

if(cur.r != null) s.push(cue.r);

二叉树层序遍历(广度优先搜索)

posted @ 2024-11-11 20:54  小杭呀  阅读(14)  评论(0)    收藏  举报