91刷题记录

Day1 -989. 数组形式的整数加法

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/add-to-array-form-of-integer/

前置知识

  • 数组的遍历

题目描述

对于非负整数 X 而言,X 的数组形式是每位数字按从左到右的顺序形成的数组。例如,如果 X = 1231,那么其数组形式为 [1,2,3,1]。

给定非负整数 X 的数组形式 A,返回整数 X+K 的数组形式。

 

示例 1:

输入:A = [1,2,0,0], K = 34
输出:[1,2,3,4]
解释:1200 + 34 = 1234
示例 2:

输入:A = [2,7,4], K = 181
输出:[4,5,5]
解释:274 + 181 = 455
示例 3:

输入:A = [2,1,5], K = 806
输出:[1,0,2,1]
解释:215 + 806 = 1021
示例 4:

输入:A = [9,9,9,9,9,9,9,9,9,9], K = 1
输出:[1,0,0,0,0,0,0,0,0,0,0]
解释:9999999999 + 1 = 10000000000
 

提示:

1 <= A.length <= 10000
0 <= A[i] <= 9
0 <= K <= 10000
如果 A.length > 1,那么 A[0] != 0

思路

1.对每个分位进行叠加(注意进位以及分位存在判断的条件)
2.最后一个进位的判断

class Solution {
    public List<Integer> addToArrayForm(int[] num, int k) {
        // 这是一个典型的使用数组(字符串)的加法魔板
        // 使用双向链表储存结果集
        LinkedList<Integer> res = new LinkedList<>();
        int len = num.length - 1;
        // 定义进位,初始默认为0
        int carry = 0;
        // 进行循环判断
        // 数组下标以0开始,k以0为分界点
        while(len>=0||k!=0){
            // 定义数组num和k 的每个分位
            int x = len>=0 ? num[len] : 0;
            int y = k!=0 ? k%10 : 0;
            int sum  = x + y + carry;
            // 存入双向链表中
            res.addFirst(sum % 10);
            // 更新进位
            carry = sum / 10;
            // 分位进行移动
            len--;
            k /=10;
        }
        // 若最后的进位不为0,则头部补充
        if(carry!=0){
            res.addFirst(carry);
        }
        // 返回结果集
        return res;
    }
}

Day2- 821. 字符的最短距离

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/shortest-distance-to-a-character

前置知识

  • 数组的遍历(正向遍历和反向遍历)

题目描述

给定一个字符串 S 和一个字符 C。返回一个代表字符串 S 中每个字符到字符串 S 中的字符 C 的最短距离的数组。

示例 1:

输入: S = "loveleetcode", C = 'e'
输出: [3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0]
说明:

- 字符串 S 的长度范围为 [1, 10000]。
- C 是一个单字符,且保证是字符串 S 里的字符。
- S 和 C 中的所有字母均为小写字母。

思路1

  • 暴力解法
    把包含字符c的下标储存在零时数组,再遍历原有数组,与零时数组进行比较(取距离最小值)
class Solution {
    public int[] shortestToChar(String s, char c) {
        int len = s.length();
        int[] res = new int[len];
        int[] temp = new int[len];
        int x = 0;
        // 把包含字符c的下标储存在零时数组
        for(int i = 0; i < len; i++){
            if(s.charAt(i)==c){
                temp[x++] = i;
            }
        }
        // 在原字符串中与零时数组进行比较
        int min = len;
        for(int i = 0; i < len; i++){
            // 这里使用x,x是有效位数
            for(int j = 0; j < x; j++){
                min = Math.min(min,Math.abs(i-temp[j]));
            }
            res[i] = min;
//            每次都要重置min值
            min = len;
        }
        return res;

    }

}

复杂度分析

  • 时间复杂度o(N^2)
  • 空间复杂度o(N)

思路2

  • 正反向数组遍历
    正向遍历,寻找每个位置到最近的c的最小值(向右搜索),反向遍历,则是向左搜索,更新每个位置到最近的c的最小值
class Solution {
    public int[] shortestToChar(String s, char c) {
        // 使用正向、反向遍历法
        int len = s.length();
        // 定义结果数组
        int[] res = new int[len];

        // 正向遍历,向右搜索
        // 定义一个标识变量prev,用于标记距离
        int prev = -100000;
        for(int i = 0; i < len; i++){
            // 发现目标字符c,则prev标记为当前的i
            if(s.charAt(i)==c)   prev = i;
            // 标记当前位置距离
            res[i] = i - prev;
        }
        // 反向遍历,向左搜索
        // prev标记为 100000
        prev = 100000;
        for(int i = len - 1; i >= 0; i--){
            // 发现目标字符c,则prev标记为当前的i
            if(s.charAt(i)==c)   prev = i;
            // 使用min函数更新当前位置距离
            res[i] = Math.min(res[i],prev - i);
        }
        // 返回结果集
        return res;
    }

}

复杂度分析

  • 时间复杂度o(N)
  • 空间复杂度o(N)

Day3 -1381. 设计一个支持增量操作的栈

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/design-a-stack-with-increment-operation/

前置知识

  • 前缀和

题目描述

请你设计一个支持下述操作的栈。

实现自定义栈类 CustomStack :

CustomStack(int maxSize):用 maxSize 初始化对象,maxSize 是栈中最多能容纳的元素数量,栈在增长到 maxSize 之后则不支持 push 操作。
void push(int x):如果栈还未增长到 maxSize ,就将 x 添加到栈顶。
int pop():弹出栈顶元素,并返回栈顶的值,或栈为空时返回 -1 。
void inc(int k, int val):栈底的 k 个元素的值都增加 val 。如果栈中元素总数小于 k ,则栈中的所有元素都增加 val 。


示例:

输入:
["CustomStack","push","push","pop","push","push","push","increment","increment","pop","pop","pop","pop"]
[[3],[1],[2],[],[2],[3],[4],[5,100],[2,100],[],[],[],[]]
输出:
[null,null,null,2,null,null,null,null,null,103,202,201,-1]
解释:
CustomStack customStack = new CustomStack(3); // 栈是空的 []
customStack.push(1); // 栈变为 [1]
customStack.push(2); // 栈变为 [1, 2]
customStack.pop(); // 返回 2 --> 返回栈顶值 2,栈变为 [1]
customStack.push(2); // 栈变为 [1, 2]
customStack.push(3); // 栈变为 [1, 2, 3]
customStack.push(4); // 栈仍然是 [1, 2, 3],不能添加其他元素使栈大小变为 4
customStack.increment(5, 100); // 栈变为 [101, 102, 103]
customStack.increment(2, 100); // 栈变为 [201, 202, 103]
customStack.pop(); // 返回 103 --> 返回栈顶值 103,栈变为 [201, 202]
customStack.pop(); // 返回 202 --> 返回栈顶值 202,栈变为 [201]
customStack.pop(); // 返回 201 --> 返回栈顶值 201,栈变为 []
customStack.pop(); // 返回 -1 --> 栈为空,返回 -1


提示:

1 <= maxSize <= 1000
1 <= x <= 1000
1 <= k <= 1000
0 <= val <= 100
每种方法 increment,push 以及 pop 分别最多调用 1000 次

思路

  • 利用数组来模拟栈,注意栈满不进栈,栈空不进栈
class CustomStack {

// 利用数组来模拟栈
// 栈:栈空不出栈,栈满不进栈

    int[] data;
    int top = -1;
    public CustomStack(int maxSize) {
        data = new int[maxSize];
    }
    
    public void push(int x) {
        // 判断是否栈满
        if(top  == data.length - 1) return ;
        data[++top] = x;
    }
    
    public int pop() {
        // 判断是否栈空
        if(top == -1) return -1;
        int val = data[top--];
        return val;
    }
    
    public void increment(int k, int val) {
        // 储存 k 和 栈容量的最小值
        int min = Math.min(k,top+1);
        for(int i = 0; i < min; i++){
            data[i] += val; 
        }
    }
}

/**
 * Your CustomStack object will be instantiated and called as such:
 * CustomStack obj = new CustomStack(maxSize);
 * obj.push(x);
 * int param_2 = obj.pop();
 * obj.increment(k,val);
 */

复杂度分析

  • 时间复杂度 pop.push O(1) inc O(N)
  • 空间复杂度 O(1)

Day4 - 394. 字符串解码

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/decode-string/

前置知识

  • 括号匹配

题目描述

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

 

示例 1:

输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:

输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:

输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:

输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"

思路

方法一:使用辅助栈(默写k神代码)
本题难点在于括号内嵌括号,需要从内向外生成于拼接字符串,和栈的先入后出特性一致

  • 构建辅助栈,遍历s中每个字符c
    • c为数字,转化为int的数字multi(用于倍数计算)
    • c为字母,在res(零时字符串)尾部加c
    • c为[,将multi和res入栈,后分别置空置0
      • 记录此[ 前的零时结果res入栈,用于发现对应]后的拼接操作
      • 记录此[ 前的multi倍数入栈,用于发现对应]后,获取multi*[…]字符串
      • 进入到新的[后,res和multi重新记录
    • 当c为],出栈,拼接字符串res = last_res +cur_multi * res
  • 返回字符串res
class Solution {
    public String decodeString(String s) {
        // 方法一:使用辅助栈法
        // 这是一个零时字符串
       StringBuilder res = new StringBuilder(); 
    //    默认的数字倍数是0
       int multi = 0;
    //    定义储存数字倍数的双向链表
       LinkedList<Integer> stack_multi = new LinkedList<>();
    //    定义储存临时字符串的双向链表
    LinkedList<String> stack_res = new LinkedList<>();
    // 对字符串s进行逐个字符遍历
    for(Character c : s.toCharArray()){
        if(c == '['){
            // 遇到 [  则将之前的数字倍数multi和临时字符串出入链表中
            stack_multi.addLast(multi);
            stack_res.addLast(res.toString());
            // 重置数字倍数和零时字符串
            multi = 0;
            res = new StringBuilder();
        }else if( c == ']'){
            // 遇到 ] 则将数字倍数和零时字符串的尾端移除,并拼装字符串res
            // 这是内层的零时字符串,用于字符串*倍数
            StringBuilder tmp = new StringBuilder();
            // 获取尾端的倍数
            int cur_multi = stack_multi.removeLast();
            // 进行内层字符*倍数=内层零时字符串
            for(int i = 0; i < cur_multi; i++){
                tmp.append(res);
            }
            // 内层零时字符串于外层res进行拼装
            res = new StringBuilder(stack_res.removeLast()+tmp);

        }else if(c>='0'&&c<='9'){
            // 字符串可能超过10,  12 = multi * 10 + 2
            multi = multi * 10 +Integer.parseInt(c+"");
        }else{
            // 遇到普通字符,则添加到零时字符串中
            res.append(c);
        }
        
    }
    return res.toString();
    }
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度: O(N)

Day5 - 232.用栈实现队列

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/implement-queue-using-stacks/

前置知识

  • 队列

题目描述

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

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

MyQueue queue = new MyQueue();

queue.push(1);
queue.push(2);
queue.peek(); // 返回 1
queue.pop(); // 返回 1
queue.empty(); // 返回 false
说明:

你只能使用标准的栈操作 -- 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
假设所有操作都是有效的、 (例如,一个空的队列不会调用 pop 或者 peek 操作)。

思路

  • 使用两个栈left和right,
  • s1栈实际是倒序的队列,s2实际是正序的队列
class MyQueue {
    Stack<Integer> left;
    Stack<Integer> right;
 
    /** Initialize your data structure here. */
    public MyQueue() {
        left = new Stack<>();
        right = new Stack<>();
    }
     
    /** Push element x to the back of queue. */
    public void push(int x) {
        //进队的话,直接弹入左栈中
        left.push(x);
    }
     
    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        //若右栈不空,直接弹出元素
        if (!right.isEmpty()) return right.pop();
        //否则将左栈中的元素弹入右栈中
        while (!left.isEmpty()) {
            right.push(left.pop());
        }
        //最后返回右栈中的元素
        return right.pop();
    }
     
    /** Get the front element. */
    public int peek() {
        //和上面同理
        if (!right.isEmpty()) return right.peek();
        while (!left.isEmpty()) {
            right.push(left.pop());
        }
        return right.peek();
    }
     
    /** Returns whether the queue is empty. */
    public boolean empty() {
        //进行判断左右两栈
        return left.isEmpty() && right.isEmpty();
    }
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */

复杂度分析

  • 时间复杂度:O(N),每次都要倒腾栈中元素
  • 空间复杂度:O(N),申请了两个栈

Day6 - 768.最多能完成排序的块②

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/max-chunks-to-make-sorted-ii/

前置知识

  • 队列

题目描述

这个问题和“最多能完成排序的块”相似,但给定数组中的元素可以重复,输入数组最大长度为2000,其中的元素最大为10**8。

arr是一个可能包含重复元素的整数数组,我们将这个数组分割成几个“块”,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。

我们最多能将数组分成多少块?

示例 1:

输入: arr = [5,4,3,2,1]
输出: 1
解释:
将数组分成2块或者更多块,都无法得到所需的结果。
例如,分成 [5, 4], [3, 2, 1] 的结果是 [4, 5, 1, 2, 3],这不是有序的数组。
示例 2:

输入: arr = [2,1,3,4,4]
输出: 4
解释:
我们可以把它分成两块,例如 [2, 1], [3, 4, 4]。
然而,分成 [2, 1], [3], [4], [4] 可以得到最多的块数。
注意:

arr的长度在[1, 2000]之间。
arr[i]的大小在[0, 10**8]之间。

思路

使用单调栈(后续刷题中还会遇到单调队列)

  • 排序块的定义:此块中最大数字为head,若此块后面数字都>=head,则此块为排序块
  • 非排序快内部进行合并操作,需要进行循环判断(弹栈)
  • 最后返回栈内个数(排序块的个数)
class Solution {
    public int maxChunksToSorted(int[] arr) {
        // 使用单调栈解决问题
        /**
        首先是排序块的概念,(后一个值>=前一个值,就会成立一个新的排序块)
        否则,就要进行融合(内部分块),在内部分块的时候,需要进行判断分块里最大值临界点
        (不可以大于上一个块内的max值,因为这会是一个新的排序块)
        */

        Stack<Integer> s = new Stack<>();
        // 进行遍历判断(栈不空,并且后一个值<前一个值,则进入内部分块环节)
        for(int num : arr){
            if(!s.empty()&&num<s.peek()){
                // 临时储存head值
                int head = s.peek();
                // 进行内部的循环分块
                while(!s.empty()&&num<s.peek()){
                    // 把栈内元素弹出(因为在内部,这不是最大的max值)
                    s.pop();
                }
                // 将最大值head储存到栈中
                s.push(head);

            }else{
                // 否则就是入栈,是一个新的排序块
                s.push(num);
            }
      	}
        // 返回栈内元素个数
        return s.size();

    }
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

Day7 - 61.旋转链表

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/rotate-list/

前置知识

暂无

题目描述

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

示例 1:

输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
示例 2:

输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL

思路

  • 使用快慢指针,快指针先走k步,当快指针到达结尾,慢指针就是倒数第k+1个位置
  • 对于循环问题(类似跑道的位置问题)使用取余方法
  • 在进行截断位置的过程中,注意顺序,否则会导致链表为空
  • 旋转k次,本质就是将倒数第k个节点置为头节点,倒数第k+1个节点置为尾结点
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode rotateRight(ListNode head, int k) {
// 考察双指针:快慢指针
        /**
        分析:向右移动k个位置,实际上就是将倒数的第k个位置进行截断
        第k+1个位置.next = null,最后一个位置.next=head
        */
        if(head==null||head.next==null){
            return head;
        }
        // 获取链表长度
        ListNode temp = head;
        int count = 0;
        while(temp!=null){
            count++;
            temp = temp.next;
        }
        // 对k取余(跟跑道赛跑一样,这是一个循环量)
        k = k % count;
        // 定义快慢指针
        ListNode fast,slow;
        fast = slow = head;
        // 快指针比慢指针先走k步(这个很关键,通过--k来设置快慢步伐)
        while(fast.next!=null){
            if(--k<0){
                slow = slow.next;
            }
            fast = fast.next;
        }
        // 此时的slow就是倒数的k+1位置
        // 一定要让fast后面先连上head
        fast.next = head;
        ListNode res = slow.next;
        slow.next = null;       
        return res;
    }
    
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

Day8 - 24.两两交换链表中的节点

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/swap-nodes-in-pairs/

前置知识

暂无

题目描述

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

 

示例 1:

image

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

输入:head = []
输出:[]
示例 3:

输入:head = [1]
输出:[1]
 

提示:

链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100

思路

  • 使用链表的交换节点操作(需要创建虚拟节点) —》链表中涉及操作的话,一般是要创建虚拟头节点的
  • 这是一个重复的问题,使用递归(递归的核心在于写出递归出口)
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        // 考察对链表的操作:如何交换节点以及虚拟节点的使用        
        if(head==null||head.next==null){
            return head;
        }
        // 创建虚拟节点和结果链表等等
        ListNode virtual = new ListNode(-1);
        ListNode res,firstNode,secondNode,nextNode;
        firstNode = head;
        // 虚拟节点的next指向head节点
        virtual.next = head;
        // 结果链表指向head.next
        res = head.next;
        // 进行交换节点操作
        // while循环体这样写是为了保证链表至少有两个元素存在!
        while(firstNode!=null&&firstNode.next!=null){
            // 注意不要让链表为空(画草图,写出正确的指针顺序)
            // 进行初始化
            secondNode = firstNode.next;
            nextNode = secondNode.next;
            // 开始交换操作
            firstNode.next = nextNode;
            secondNode.next = firstNode;
            //这一步终于显示出虚拟头节点的作用了(用于连接节点,具体可画图验证)
            //当我们要操作一个节点时,一般需要两个节点进行辅助,一个是前驱节点,另一个就是后驱节点,这里的虚拟节点就相当于前驱节点
            virtual.next = secondNode;
            // 虚拟节点回到first位置
            virtual = firstNode;
            // first移动到next位置,这样就可以循环迭代了
            firstNode = nextNode;
        }
        return res;        
    }
}

Day9 - 109.有序链表转换二叉搜索树

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/convert-sorted-list-to-binary-search-tree/

前置知识

  • 递归
  • 二叉搜索树的任意一个节点点,当前节点的值必然大于所有左子树节点的值。同理,当前节点的值必然小于所有右子树节点的值

题目描述

给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

示例:

给定的有序链表: [-10, -3, 0, 5, 9],

一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:

    0
   / \
 -3   9
 /   /
-10  5

思路

快慢指针+递归

  • 二叉树 -》第一想法是递归

  • 链表是有序链表

  • 二叉平衡树的中序遍历得到的是一组有序的数

  • 使用递归,root = 链表mid值, left = 链表左侧,right = 链表右侧

  • 通过快慢指针法进行定位mid值(慢指针每次走一步,快指针每次走两步,当快指针走完所有的节点时候,慢指针的位置就是中间节点的位置),通过穿针引线法进行切割左右子树

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
// class Solution {
//     public TreeNode sortedListToBST(ListNode head) {
        /**
        分析:
        BST的根节点应该是链表正中间的那个节点。
		用快慢指针的方法找到链表正中间的节点(如果是偶数的话,取正中间的两个节点之一),左子树就是链表左半部分,右子树就是右半部分。
		把链表从中间节点之前断开,递归调用方法构建BST。递归的出口是当链表为空(返回空指针)或者只有一个链表节点(直接返回根节点)。
        */
        // 注意编程的完整性(如果head是null,后面操作全是扯淡,并且会报空指针异常!!!)
         if(head==null){
             return null;
         }
        // 链表中间节点就是根节点root
         ListNode mid = findMid(head);
         // 进行中序遍历,构造二叉平衡树
         TreeNode root = new TreeNode(mid.val);
         // 这个是递归出口 mid==head时候,进行回溯(其实就是中间节点等于节点本身,也就是只有一个节点的时候,返回该节点)
         if(mid == head){
             // 回溯,返回节点
             return root;
         }
         //在 findMid(head)中已经对左子树切割了,所以左子树的递归传入参数是head
         root.left = sortedListToBST(head);
		 //右子树也是一个完整的链表,递归传入参数为mid.next
         root.right = sortedListToBST(mid.next);
         // 返回根节点
         return root;
    }
     // 找到链表的中间节点
     public ListNode findMid(ListNode head) {
         ListNode fast,slow;
         ListNode prev = null;
         fast = slow = head;
         // 注意循环体内的写法,可以使用图解理解
         while(fast!=null&&fast.next!=null){
             // 定义slow指针的前驱
            prev = slow;
             // 慢指针走一步
             slow = slow.next;
             // 快指针走两步
             fast = fast.next.next;
         }
         // 切割左边链表
         if(prev!=null){
            prev.next = null;
        }
         // 不需要切割右边链表
         // 此时慢指针就是链表的中间节点
         return slow;
     }
}

复杂度分析

  • 时间复杂度:O(Nlog(N))N为链表长度,每N/2个节点在每次递归都要遍历一次
  • 空间复杂度:O(log(N))递归栈深度是树的高度。

代码优化

  • 上面的代码中,使用了prev进行进行切割左树,在实际中,可以使用类似归并排序的方法,不进行切割
  • 这种方式是递归写法的变种,通过增加参数,达到想到的目的,使用场景十分广泛(例如本题中,划分左右子树需要头尾节点,那么在构造递归参数的时候,就增加了(ListNode head, ListNode tail)参数)
class Solution {
    public TreeNode sortedListToBST(ListNode head) {
        if(head==null){
            return null;
        } 
        return toBST(head,null);
    }
//快慢指针
    public TreeNode toBST(ListNode head, ListNode tail){
        ListNode slow = head;
        ListNode fast = head;
        //递归出口,head==tail说明,链表区间已经遍历构造完毕,进行回溯即可
        if(head==tail) return null;
        while(fast!=tail&&fast.next!=tail){
        fast = fast.next.next;
        slow = slow.next;
        }
//merge sort 简化版代码,有点相似
        TreeNode treehead = new TreeNode(slow.val);
        treehead.left = toBST(head,slow);
        treehead.right = toBST(slow.next,tail);
        return treehead;
    }
}

Day10 - 160.相交链表

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/intersection-of-two-linked-lists/

前置知识

  • 链表
  • 双指针

题目描述

编写一个程序,找到两个单链表相交的起始节点。

思路

  • 使用哈希表法(相交节点的本质是哈希表中储存的地址一致,这里使用hashset 而不是hashmap)
  • 使用双指针法(本质是一个追赶问题,A+B+C = B+C+A)
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        /*
        分析:使用哈希表法(因为节点在引用的时候是根据地址来链接的)
        - 先将A遍历,储存在哈希表中
        - 遍历B,同时在哈希表中搜索有没有相应的值(哈希值一致),那么就找到相交节点了
        */
        // HashSet和HashMap的区别:
        // HashMap储存键值对	              HashSet仅仅存储对象
        // 使用put()方法将元素放入map中	   使用add()方法将元素放入set中
       HashSet<ListNode> hash  = new HashSet<>();
       while(headA!=null){
            // 将A中节点储存哈希表中
            hash.add(headA);
            headA = headA.next;
        }
        // 遍历表B
        while(headB!=null){
            if(hash.contains(headB)){
                return headB;
            }
            headB = headB.next;
        }
        return null;
    }
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(N)
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        /**
        分析:本质是一个追赶问题
        使用双指针法,若相交,那么链表1 可以表示成A+C ,链表2 可以表示成B+C
        相交处:A+C+B = B+C+A,说明两指针相遇时,该指针指向节点就是相交节点
        不相交时:由于两者都会同时达到对方链表的null处,不论A+C+B还是B+C+A,最后都是到达另一边的null处,最后返回任意一个指针(null)
        */
        ListNode p1,p2;
        p1 = headA;p2 = headB;
        // 循环终止条件是p1==p2
        while(p1!=p2){
            // 核心代码,A走完,走向B
            p1 = p1==null ? headB : p1.next;
            // B走完,走向A
            p2 = p2==null ? headA : p2.next;
        }
        // 返回任意一个指针
        return p1;
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

Day11 - 142.环形链表②

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/linked-list-cycle-ii/

前置知识

暂无

题目描述

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

进阶:

你是否可以使用 O(1) 空间解决此题?

思路

  • 使用哈希表法
  • 使用快慢指针法

Java代码(快慢指针法)

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
 /**
        分析:
        - pos不是参数,是需要自己进行判断环的初始位置
        - 判断环的存在,使用快慢指针法。当快指针和慢指针重合,该点为重合点
         */
         ListNode fast,slow;
        //  快慢指针
         fast = slow = head;       
         while(true){
             //节点为空或者单节点的时候,非环 
            if(fast==null||fast.next==null){
             return null;
            }
            slow = slow.next;
            fast = fast.next.next;
            if(fast==slow){
            //  两个指针相遇了,说明有环的存在(这是第一轮相遇)
            // 但是,这个环起始节点在哪还是不知道(构建第二轮相遇)
                 break;
             }
        }
        // 将fast指针,指向head,slow指针和fast指针都移动一格,直到相遇
        fast = head;
        while(fast!=slow){
        fast = fast.next;
        slow = slow.next;
        }
        //最后就是环的入口起点,有相关的数学公式可以证明
        return fast;
    }
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度: O(1)

Java代码(哈希表法)

//使用哈希表法,这里只能用引用型的哈希set
     HashSet<ListNode> set = new HashSet<>();
    //当head不空
     while(head!=null){
//表中如果包含的话,那么,直接返回
         if(set.contains(head)){
             return head;
         }
         //  否则,储存到哈希表中,head移动一步
        set.add(head);
        head = head.next;
     }
//非环,那么就是null
     return null;
     } 

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度: O(N) 用到了哈希表

Day12 - 146.LRU缓存机制

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/lru-cache/

前置知识

暂无

题目描述

运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:

LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。


进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?



示例:

输入

["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释

LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

思路

使用双向链表和哈希表(有点难,跟着答案敲一遍还是会出错)

class LRUCache {
    /**
    分析:这是一个设计题,难度较大。
    对数据进行增删改操作,并且时间复杂度为O(1)====》双向链表
    对数据进行查找操作,并且时间复杂度为O(1)====》哈希表
    最后选择的数据结构是 哈希表+双向链表(伪头部和伪尾部法),这个伪头部和伪尾部法具有巨大的威力
    主要的业务逻辑,在代码注释中说明
     */

    // 数据结构为 双向链表,包含了键值对,前驱后驱节点,构造函数
    class DLinkedNode{
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        DLinkedNode(){
        }
        DLinkedNode(int _key,int _value){
            key = _key;
            value = _value;
        }
    }
    // 数据结构为 哈希表,用来查找
    HashMap<Integer,DLinkedNode> cache = new HashMap<>();
    // 定义链表的长度,伪头部,伪尾部
    private int size;
    private DLinkedNode head,tail;
    // 定义LRUCache的容量(哈希表里面是不需要设置容量的,整体设置就行)
    private int capacity;
    // LRUCache的构造函数
    public LRUCache(int capacity) {
        // 初始化的时候,让链表长度为0
        this.size = 0;
        // 初始化capacity
        this.capacity = capacity;
        // 初始化伪头部和伪尾部信息
        head = new DLinkedNode();
        tail = new DLinkedNode();
        // 伪头部和伪尾部进行连接
        head.next = tail;
        tail.prev = head;
    }
    /**
    核心思想:
    在哈希表中查找,找到就返回相应的节点值(同时将该节点移动到链表头部),未找到就返回-1
     */
    public int get(int key) {
        // 调用哈希表的查找函数
        DLinkedNode res = cache.get(key);
        if(res==null){
            // 未找到节点
            return -1;
        }
        // 调用内部类的移动到头部函数(自己写,返回void),注意:原有节点也要删除
        moveToHead(res);
        // 返回节点值
        return res.key;

    }
    
    public void put(int key, int value) {
    /**
    核心思想:
    在哈希表中查找,找到就更新相应的节点值(同时将该节点移动到链表头部)
    未找到就新建一个(同时将该节点移动到链表头部)
    若缓存容量达到上限,就删除尾部的哈希节点(对应到链表节点中)
    */
    // 调用哈希表的查找函数(自己写,返归节点)
        DLinkedNode res = cache.get(key);
        if(res!=null){
            // 找到节点,更新节点信息,将节点移动到头部去
            res.value = value;
            moveToHead(res);
        }else{
            // 未找到节点,则新建一个
            DLinkedNode newNode= new DLinkedNode(key,value);
            // 链表长度+1
            size++;
            // 添加到头部
            addToHead(newNode);
            // 添加到哈希表中
            cache.put(key,newNode);
            // 判断size和capacity的关系
            if(size>capacity){
                // 移除尾部的节点信息
                DLinkedNode tailNode = moveTailNode();
                // 删除哈希表中的项
                cache.remove(tailNode.key);
                // size-1
                size--;
            }
        }
    }
    // 内部添加到头部函数
    public void addToHead(DLinkedNode node){
        // 移动节点到头部
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    
    // 内部移动到头部函数
    public void moveToHead(DLinkedNode node){
        // 先删除原有节点
        node.prev.next = node.next;
        node.next.prev = node.prev;
        // 再移动节点到头部
        addToHead(node);
    }
    // 内部移除尾部的节点信息
    public DLinkedNode moveTailNode(){
        DLinkedNode node = tail.prev;
        node.prev.next = node.next;
        node.next.prev = node.prev;
        return node;

    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

复杂度分析

  • 时间复杂度:O(1)
  • 空间复杂度: O (N)

Day13 - 104.二叉树的最大深度

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/maximum-depth-of-binary-tree

前置知识

  • 递归

题目描述

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7],

  3
 / \
9  20
  /  \
 15   7
返回它的最大深度 3 。

思路

  • 二叉树是一个递归的数据结构
  • 最大深度使用DFS
  • 使用递归的时候,可以套用产品经理法(只考虑逻辑,不考虑实现)
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int maxDepth(TreeNode root) {
        // 初步想法使用递归
        int  depth = 0;
        if(root==null){
            return 0;
        }
        // 取左子树和右子树中的最大值
        depth = Math.max(maxDepth(root.left),maxDepth(root.right));
        // +1 是为了考虑原有节点
        return depth + 1;
         
    }
}

复杂度分析

  • 时间复杂度:O(N) 每个节点都遍历一遍
  • 空间复杂度: O (N) 不平衡二叉树的深度至少是log(N) ,最坏情况下会退化成一个链表,深度为N,因此递归情况下使用的栈空间是O(N)

Day 14 - 100.相同的树

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/same-tree/

前置知识

  • 递归
  • 层序遍历
  • 前中序确定一棵树

题目描述

给定两个二叉树,编写一个函数来检验它们是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

输入:       1         1
        / \       / \
       2   3     2   3

      [1,2,3],   [1,2,3]

输出: true
示例 2:

输入:      1          1
        /           \
       2             2

      [1,2],     [1,null,2]

输出: false
示例 3:

输入:       1         1
        / \       / \
       2   1     1   2

      [1,2,1],   [1,1,2]

输出: false

思路

使用递归

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if(p==null&&q==null){
            return true;
        }
        if(p==null||q==null){
            return false;
        }
        if(p.val!=q.val){
            return false;
        }
        return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
        

    }
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(N),最坏情况下是遍历一个链表N

Day15 - 129.求根到叶子节点数字之和

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/

前置知识

  • DFS
  • BFS
  • 前序遍历

题目描述

给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。

例如,从根到叶子节点路径 1->2->3 代表数字 123。

计算从根到叶子节点生成的所有数字之和。

说明: 叶子节点是指没有子节点的节点。

示例 1:

输入: [1,2,3]
  1
 / \
2   3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
示例 2:

输入: [4,9,0,5,1]
  4
 / \
9   0
 / \
5   1
输出: 1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495.
从根到叶子节点路径 4->9->1 代表数字 491.
从根到叶子节点路径 4->0 代表数字 40.
因此,数字总和 = 495 + 491 + 40 = 1026.

思路

  • dfs,root结点与左右子树有关,所以构造一个新的函数(带参),将参数不断跟新传递,以达到操作左右子树的目的
  • bfs,使用两个队列储存节点和节点值信息,不断将其组合(若遇叶子节点,则将结果出队累加到sum中,若非叶子节点,则继续进行组合遍历左右子树)
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
//# Java代码(DFS)
class Solution {
    // 这是结果
    private int ans;
    public int sumNumbers(TreeNode root) {
        // 初步想法使用递归:用labuladong的框架思维
        // root节点的操作不是简单的进行左右孩子操作,而是进行左右子树操作
        // 根据labuladong的框架思维,可构造一个新的函数(多带一个参数)
        
        dfs(root,0);
        // 调用dfs函数(新的函数)
        return ans;
    }
    public void dfs(TreeNode root, int last){
        if(root == null) return;
        if(root.left == null && root.right == null) {
            // 记住这个操作,last * 10 + root.val,在递归中可以自底向上形成数字
            ans += last * 10 + root.val;
            return;
        }
        // 每次的last参数要跟新,即为last * 10 + root.val
        dfs(root.left, last * 10 + root.val);
        dfs(root.right, last * 10 + root.val);
    }
}

复杂度分析

  • 时间复杂度:O(N),n是二叉树的节点个数,对每个节点访问一次
  • 空间复杂度:O(N),n是二叉树的节点个数,空间复杂度主要取决于递归调用的栈空间,递归栈的深度等于二叉树的高度,最坏情况下,二叉树的高度等于节点个数
 public int sumNumbers(TreeNode root) {
    //    使用Bfs
        
        // 定义结果sum
        int sum = 0;
        // 构建两个队列,存储节点信息和节点值信息
        Queue<TreeNode> nodeQueue = new LinkedList<>();
        Queue<Integer> numQueue = new LinkedList<>();
        
        // 节点进队尾
        nodeQueue.offer(root);
        numQueue.offer(root.val);
        // 进行遍历判断 若节点队内不空
        while(!nodeQueue.isEmpty()){
            // 节点队内 元素出队头,储存临时节点位置
            TreeNode node = nodeQueue.poll();
            int num = numQueue.poll();
            TreeNode left = node.left,right = node.right;
            // 若为叶子节点,那么进行累加到sum中
            if(left==null&&right==null){
                sum += num;
            }else{
                // 否则,进行遍历左右子树
                if(left!=null){
                    // 左子树不空,则进队
                    nodeQueue.offer(left);
                     numQueue.offer(num * 10 + left.val);
                }
                if(right!=null){
                    // 右子树不空,则进队
                    nodeQueue.offer(right);
                     numQueue.offer(num * 10 + right.val);
                }
                
            }
        }
        return sum;
        
    }

复杂度分析

  • 时间复杂度:O(N)其中 nn 是二叉树的节点个数。对每个节点访问一次。
  • 空间复杂度:O(N)其中 nn 是二叉树的节点个数。空间复杂度主要取决于队列,每个队列中的元素个数不会超过 nn

Day16 - 513.找树左下角的值

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/find-bottom-left-tree-value/

前置知识

暂无

题目描述

给定一个二叉树,在树的最后一行找到最左边的值。

示例 1:

输入:

  2
 / \
1   3

输出:
1
 

示例 2:

输入:

      1
     / \
    2   3
   /   / \
  4   5   6
     /
    7

输出:
7

思路

  • 使用BFS
  • 使用DFS

Java代码(BFS)

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int findBottomLeftValue(TreeNode root) {
        // 根据题目的意思很容易想到使用层序遍历
        //  找到树的最后一行的第一个节点,即为满足要求的节点
        // 使用双向链表来定义队列
        LinkedList<TreeNode> deque = new LinkedList<>();
        // 默认root非空,将其添加到队列中
        deque.addFirst(root);
        // 定义结果res
        int res = 0;
        // 当队列非空,对每层的节点依次进队
        while(!deque.isEmpty()){
            // 定义队列内元素的大小
            int size = deque.size();
            // 这是令res等于该行的第一个节点值
            for(int i = 0; i < size; i++){
                // 出队,取队列内节点
                TreeNode node = deque.pollFirst();
                if(i == 0){
                    res = node.val;
                }
                // 如果左节点不空,那么左节点进队
                if(node.left!=null){
                    deque.addLast(node.left);
                }
                // 如果右节点不空,那么右节点进队
                if(node.right!=null){
                    deque.addLast(node.right);
                }
            }
            
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度:O(N),其中N是树的节点
  • 空间复杂度:O(Q),Q为队列长度,最坏情况下是满二叉树,与N同阶,N为树的节点总数

Java代码(DFS)

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    // 定义当前最大深度和相应值
    private int curDepth = -1;
    private int curVal = 0;
    public int findBottomLeftValue(TreeNode root) {
        dfs(root,0);
        return curVal;

    }
    public void dfs(TreeNode root,int depth){
        if(root==null){
            // 这是递归出口
            return ;
        }
        // 前序遍历的主逻辑
        if(curDepth < depth){
            // 深度更新
            curDepth = depth;
            // 值更新
            curVal = root.val;
        }
        // 调用左右子树
        dfs(root.left,depth+1);
        dfs(root.right,depth+1);
    }
}

复杂度分析

  • 时间复杂度:O(N),N为树的节点树
  • 空间复杂度:O(H),h为树的高度

Day17 - 297.二叉树的序列化和反序列化

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/

前置知识

暂无

题目描述

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

示例: 

你可以将以下二叉树:

  1
 / \
2   3
   / \
  4   5

序列化为 "[1,2,3,null,null,4,5]"
提示: 这与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

说明: 不要使用类的成员 / 全局 / 静态变量来存储状态,你的序列化和反序列化算法应该是无状态的。

思路

  • 序列化可以使用前序遍历将每个节点储存起来,注意遇到节点的左右孩子为空时,需要标记起来(可以标记为“null”或“x”),其中为了避免反序列化出现NumberFormatException现象,可以标记为“null,”或“x,”
  • 反序列化可以将string拆分成string数组,并且储存到链表中,取链表中元素,先序遍历构造二叉树
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        // 序列化采用前序遍历
        // 如果遍历遇到空,那么标记成 “null”字符串
        if(root==null){
            // 在这里 null后面一定要有 ,  否则在解析过程会出问题
            return "null,";
        }
        // 进行前序遍历 并且拼接
        return root.val+","+serialize(root.left)+serialize(root.right);
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        // 反序列化思路也是使用先序遍历
        // 将String类型的data数据储存到链表中(数据结构认定为队列)
        // data.spilit(",")将string拆分多个字符串数组,使用Array.asList()可以将数组转换为list
        Queue<String> queue = new LinkedList<>(Arrays.asList(data.split(",")));
        return dfs(queue);
    }
    private TreeNode dfs(Queue<String> queue ){
        // 取队列节点 判断是否为null
        String val = queue.poll();
        if("null".equals(val)){
            return null;
        }
        // 非null值 则构造根节点
        TreeNode root = new TreeNode(Integer.parseInt(val));
        // 递归创建左子树
        root.left = dfs(queue);
        // 递归创建右子树
        root.right = dfs(queue);
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));

复杂度分析

  • 时间复杂度:O(N) 需要遍历树的每个节点
  • 空间复杂度:O(N) 最坏情况下,调用n次递归栈

Day18 - 987.二叉树的垂序遍历

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/vertical-order-traversal-of-a-binary-tree

前置知识

  • DFS
  • 排序

题目描述

给定二叉树,按垂序遍历返回其结点值。

对位于 (X, Y) 的每个结点而言,其左右子结点分别位于 (X-1, Y-1) 和 (X+1, Y-1)。

把一条垂线从 X = -infinity 移动到 X = +infinity ,每当该垂线与结点接触时,我们按从上到下的顺序报告结点的值(Y 坐标递减)。

如果两个结点位置相同,则首先报告的结点值较小。

按 X 坐标顺序返回非空报告的列表。每个报告都有一个结点值列表。



示例 1:



输入:[3,9,20,null,null,15,7]
输出:[[9],[3,15],[20],[7]]
解释:
在不丧失其普遍性的情况下,我们可以假设根结点位于 (0, 0):
然后,值为 9 的结点出现在 (-1, -1);
值为 3 和 15 的两个结点分别出现在 (0, 0) 和 (0, -2);
值为 20 的结点出现在 (1, -1);
值为 7 的结点出现在 (2, -2)。
示例 2:



输入:[1,2,3,4,5,6,7]
输出:[[4],[2],[1,5,6],[3],[7]]
解释:
根据给定的方案,值为 5 和 6 的两个结点出现在同一位置。
然而,在报告 "[1,5,6]" 中,结点值 5 排在前面,因为 5 小于 6。


提示:

树的结点数介于 1 和 1000 之间。
每个结点值介于 0 和 1000 之间。

思路

  • 对树进行遍历,这里使用DFS,前序遍历
  • 对y坐标,x坐标,节点值进行依次排序,java使用Collection集合类排序
  • 以 Y:[X :[val1,val2,…]]为模型,使用哈希表数据结构
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    /**
    分析:
    - 对树进行遍历,这里使用DFS,前序遍历
    - 对y坐标,x坐标,节点值进行依次排序,java使用Collection集合类排序
    - 以 Y:[X :[val1,val2,...]]为模型,使用哈希表数据结构
     */
    //  定义数据结构--哈希表Y:[X :[val1,val2,...]]
    Map<Integer,Map<Integer,List<Integer>>> hashMap =  new HashMap<>();
    public List<List<Integer>> verticalTraversal(TreeNode root) {
        // 初始坐标是 (0,0)
        dfs(root,0,0);
        // 定义所需要的结果集 ---》[val1,val2,...]
        List<List<Integer>> res = new ArrayList<>() ;
        // 定义Y坐标的临时储存结果,这里使用hashMap.keySet(),生成hashMap键值
        List<Integer> pos_Y = new ArrayList<>(hashMap.keySet());
        // 调用Collections集合的sort函数,进行排序
        Collections.sort(pos_Y);
        // 遍历pos_Y其中的值,开启逐层拆分
        for(Integer i:pos_Y){
            // 定义临时数据 --[val1,val2,...]
            List<Integer> temp = new ArrayList<>();
            // 定义X坐标的临时储存结果,这里使用hashMap.keySet(),生成hashMap键值
            List<Integer> pos_X = new ArrayList<>(hashMap.get(i).keySet());
            // 调用Collections集合的sort函数,进行排序
            Collections.sort(pos_X);
            // 遍历pos_X其中的值,开启逐层拆分
            for(Integer j :pos_X){
                List<Integer> values = hashMap.get(i).get(j);
                // 调用Collections集合的sort函数,进行排序(这是对node节点中的值排序了)
                Collections.sort(values);
                // 排序结束,那么就应该添加到temp集中,由于可能是多个值,所以调用addAll
                temp.addAll(values);
            }
            // 最内层的temp集收集好values值后,应该添加到res结果集中
            res.add(temp);
        }
        // 最后返回结果集
        return res;
    }
    // 前序遍历 传入坐标值x,y
    private void dfs(TreeNode root,int x,int y){
        if(root==null){
            //递归出口
            return ;
        }
        // 前序遍历 主逻辑
        // 先判断是否包含Y坐标
        if(!hashMap.containsKey(y)){
            // 没有就将y添加进去
            hashMap.put(y,new HashMap<>());
        }
         // 后判断是否包含Y坐标中是否有x坐标
        if(!hashMap.get(y).containsKey(x)){
            // 没有就将x添加进去
            hashMap.get(y).put(x,new ArrayList<>());
        }
        // 将root节点中的值储存到hashMap中
        hashMap.get(y).get(x).add(root.val);
        // 左右子树遍历
        dfs(root.left,x+1,y-1);
        dfs(root.right,x+1,y+1);
    }
}

复杂度分析

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)

注意点

  • 注意本题中的嵌套,数据结构的选择,排序的顺序以及dfs遍历的逻辑!

Day19 - 1.两数之和

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/two-sum

前置知识

  • 哈希表

题目描述

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

思路

  • 哈希表法
class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 哈希表法
        HashMap<Integer,Integer> hashmap = new HashMap<>();
        int len = nums.length;
        for(int i = 0; i < len; i++){
            // 判断hashmap是否包含target-nums[i])
            if(hashmap.containsKey(target-nums[i])){
                // 若有,则找到并且返回
               return new int[]{i,hashmap.get(target-nums[i])}; 
            }
            // 否则储存到哈希表中
            hashmap.put(nums[i],i);
        }
        return null;

    }
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

Day20 - 347.前K个高频元素

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/top-k-frequent-elements/

前置知识

  • 哈希表
  • 堆排序
  • 快速选择

题目描述

给定一个非空的整数数组,返回其中出现频率前  k  高的元素。
示例 1:


输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]


示例 2:


输入: nums = [1], k = 1
输出: [1]


提示:

- 你可以假设给定的  k  总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 O(n log n) , n  是数组的大小。
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
- 你可以按任意顺序返回答案。

思路

  • 计算每个数的频次 -》使用哈希表数据结构
  • 在生成的频次中去前K个最大 —》使用堆排序
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        /**
        分析:
        - 计算每个数的频次  -》使用哈希表数据结构
        - 在生成的频次中去前K个最大 —》使用堆排序
         */
        //  定义哈希表
        HashMap<Integer,Integer> map = new HashMap<>();
        // 遍历数组
        for(int num : nums){
            // 储存到哈希表中,键为数组值,值为个数
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        // 定义结果集
        int[] ret = new int[k];
        // 使用优先队列:是一种使用堆实现的数据结构,二叉小顶堆
        // 堆:是一种抽象的数据结构,我们在插入或删除元素后堆将自动调整结构以维持有序,可用二叉树实现。
        // (o1, o2) -> map.get(o2)-map.get(o1),通过Lambda表达式的形式传入自定义比较器构造大顶堆
        PriorityQueue<Integer> pq = new PriorityQueue<>((o1, o2) -> map.get(o2)-map.get(o1));
        /**
        比如我们往队列里面插入132,插入2的时候,就会在内部调整为123(默认顺序是升序)
         */
        // 将键储存到优先队列中(大顶堆自动排好序了)
        pq.addAll(map.keySet());
        for(int i=0;i<k;i++){
            // 取堆顶元素
            ret[i]=pq.remove();
        }
        return ret;
    }
}

复杂度分析

  • 时间复杂度:O(Nlogk)使用哈希表记录出现次数,每个元素需要O(1) 的时间,共需 O(N) 的时间,堆的大小至多为 kk,因此每次堆操作需要 O(logk) 的时间,共需 O(N\log k)O(Nlogk) 的时间。二者之和为 O(Nlogk)
  • 空间复杂度:O(N) 哈希表的开销

Day21 - 447.回旋镖的数量

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/number-of-boomerangs/

前置知识

  • 哈希表
  • 两点间距离计算方法

题目描述

给定平面上  n 对不同的点,“回旋镖” 是由点表示的元组  (i, j, k) ,其中  i  和  j  之间的距离和  i  和  k  之间的距离相等(需要考虑元组的顺序)。

找到所有回旋镖的数量。你可以假设  n 最大为 500,所有点的坐标在闭区间 [-10000, 10000] 中。

示例:


输入:
[[0,0],[1,0],[2,0]]

输出:
2

解释:
两个回旋镖为 [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]]

思路

  • 记录每个点之间的距离以及个数 --》使用哈希表数据结构+点的距离公式
  • 距离相同,对个数进行排列组合(全排列,这是有次序问题),已知一点确定另外两点,An 2 = n * (n-1)
class Solution {
    /**
        分析:
        - 记录每个点之间的距离以及个数 --》使用哈希表数据结构+点的距离公式
        - 距离相同,对个数进行排列组合,已知一点确定另外两点,Cn 2 = n * (n-1)
         */
    public int numberOfBoomerangs(int[][] points) {
       
        // 初始化回旋镖个数
        int res = 0;
        // 遍历最外层(共有几个点)
        for(int i = 0; i < points.length; i++){
            // 哈希表要在第一层for循环定义
             HashMap<Integer,Integer> hashmap = new HashMap<>();
            // 比较每个点中的距离
            for(int j = 0; j < points.length; j++){
                if(i!=j){
                    int dis = distance(points[i],points[j]);
                    //储存到哈希表中
                    hashmap.put(dis,hashmap.getOrDefault(dis,0)+1);
                }
            }
            // 遍历哈希表中,距离相同的个数
            for(int val:hashmap.values()){
                //进行排列组合,并且叠加
                res += val * (val - 1);
            }
        }
        return res;


    }
    public int distance(int[] point1,int[] point2){
        int x = point2[0] - point1[0];
        int y = point2[1] - point1[1];
        return x * x + y * y;
    }
}

复杂度分析

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n)

Day22 - 3.无重复字符的最长字串

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

前置知识

暂无

题目描述

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

思路

  • 涉及一个连续的字串问题 --》滑动窗口,从题意看,这是一个可变窗口
  • 重复问题 --》哈希表
class Solution {
    /**
    分析:
    - 涉及一个连续的字串问题 --》滑动窗口,从题意看,这是一个可变窗口
    - 重复问题              --》哈希表
     */
    public int lengthOfLongestSubstring(String s) {
        // 定义哈希表的数据结构 k-Character,v-Integer 这里表示字符在s的下标位置,
        // 目的是为了定位hashmap.get(s.charAt(r))+1
        HashMap<Character,Integer> hashmap = new HashMap<>();
        // 定义结果
        int res = 0;
        // 定义滑动窗口左右指针
        int len = s.length();
        // 可变窗口解题步骤:左右指针为0,r一直走一步,l在满足条件的时候走
        int l =0, r = 0;
        while(r < len){
             /**
            1、首先,判断当前字符是否包含在map中,如果不包含,将该字符添加到map(字符,字符在数组下标),
             此时没有出现重复的字符,左指针不需要变化。此时不重复子串的长度为:i-left+1,与原来的maxLen比较,取最大值;

            2、如果当前字符 ch 包含在 map中,此时有2类情况:
             1)当前字符包含在当前有效的子段中,如:abca,当我们遍历到第二个a,当前有效最长子段是 abc,我们又遍历到a,
             那么此时更新 left 为 map.get(a)+1=1,当前有效子段更新为 bca;
             2)当前字符不包含在当前最长有效子段中,如:abba,我们先添加a,b进map,此时left=0,我们再添加b,发现map中包含b,
             而且b包含在最长有效子段中,就是1)的情况,我们更新 left=map.get(b)+1=2,此时子段更新为 b,而且map中仍然包含a,map.get(a)=0;
             随后,我们遍历到a,发现a包含在map中,且map.get(a)=0,如果我们像1)一样处理,就会发现 left=map.get(a)+1=1,实际上,left此时
             应该不变,left始终为2,子段变成 ba才对。

             为了处理以上2类情况,我们每次更新left,left=Math.max(left , map.get(ch)+1).
             另外,更新left后,不管原来的 s.charAt(i) 是否在最长子段中,我们都要将 s.charAt(i) 的位置更新为当前的i,
             因此此时新的 s.charAt(i) 已经进入到 当前最长的子段中!
             */
            if(hashmap.containsKey(s.charAt(r))){
                // hashmap.get(s.charAt(r))+1 表示为之前出现的字符向后滑动一格
                l = Math.max(l,hashmap.get(s.charAt(r))+1);
            }
            // r-l+1 为新的结果,与原来的res比较取最大
            res = Math.max(res,r-l+1);
            // 要更新 s.charAt(r)的下标位置
            hashmap.put(s.charAt(r),r);
            // r前进一步
            r++;
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

Day23 - 30.串联所有单词的字串

入选理由

暂无

题目地址

https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words

前置知识

  • 哈希表
  • 双指针

题目描述

给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。

示例 1:
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:

输入:
s = "wordgoodgoodgoodbestword",
words = ["word","good","best","word"]
输出:[]

思路

  • 子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序–》哈希表
  • 找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置 --》滑动窗口
  • 体会哈希表的魅力
class Solution {
    /**
    分析:
    - 子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序--》哈希表
    - 找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置 --》滑动窗口
    - 体会哈希表的魅力
     */
    public List<Integer> findSubstring(String s, String[] words) {
        // 定义结果集
        List<Integer> res = new ArrayList<>();
        // 特殊情况
        if(words==null||s==""){
            return null;
        }
        // 对words中进行计数,len为每个单词的长度,n为单词个数
        int len = words[0].length();
        int n = words.length;
        // 定义哈希表 --->储存words中的单词
        HashMap<String,Integer> hashmap1 = new HashMap<>();
        // 对words进行遍历
        for(String word:words){
            hashmap1.put(word,hashmap1.getOrDefault(word,0)+1);
        }
        // 对s进行遍历(s.length() - len * n + 1 为 滑动窗口移动次数)
        for(int i = 0; i < s.length() - len * n + 1; i++){
            // 在窗口内,需要进行判定,hashmap1的数据在该窗格是否都存在 ---》hashmap2
             HashMap<String,Integer> hashmap2 = new HashMap<>();
            //  定义初始的word个数
            int num = 0;
            // 当word个数小于n时,窗格内要循环判断,终止条件时hashmap2的value大于hashmap1的value
            while(num < n){
                // 截取字符串,i相当于滑动偏移量,num*len和(num+1)*len 是窗格内每个单词截取的数据
                String str = s.substring( i + num * len,i + (num + 1) * len);
                // 储存到hashmap2
                hashmap2.put(str,hashmap2.getOrDefault(str,0)+1);
                // 若hashmap1中不包括str,说明该窗格不符合条件
                if(hashmap1.containsKey(str)){
                    // 接下来判断hashmap2中的value和hashmap1的value大小
                    if(hashmap2.get(str)>hashmap1.get(str)){
                        // 大于,则该窗格不符合(可画图验证)
                        break;
                    }
                    // 前进到下一个单词
                    num++;
                }else{
                    break;
                }
            }
            // 如果一个滑动窗口内所有单词都符合,那么添加i
            if(num==n){
                res.add(i);
            }
        }
    return res;

    }
}

复杂度分析

  • 时间复杂度:O(N^2)
    前置知识

  • 哈希表

  • 双指针

题目描述

给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。

示例 1:
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:

输入:
s = "wordgoodgoodgoodbestword",
words = ["word","good","best","word"]
输出:[]

思路

  • 子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序–》哈希表
  • 找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置 --》滑动窗口
  • 体会哈希表的魅力
class Solution {
    /**
    分析:
    - 子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序--》哈希表
    - 找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置 --》滑动窗口
    - 体会哈希表的魅力
     */
    public List<Integer> findSubstring(String s, String[] words) {
        // 定义结果集
        List<Integer> res = new ArrayList<>();
        // 特殊情况
        if(words==null||s==""){
            return null;
        }
        // 对words中进行计数,len为每个单词的长度,n为单词个数
        int len = words[0].length();
        int n = words.length;
        // 定义哈希表 --->储存words中的单词
        HashMap<String,Integer> hashmap1 = new HashMap<>();
        // 对words进行遍历
        for(String word:words){
            hashmap1.put(word,hashmap1.getOrDefault(word,0)+1);
        }
        // 对s进行遍历(s.length() - len * n + 1 为 滑动窗口移动次数)
        for(int i = 0; i < s.length() - len * n + 1; i++){
            // 在窗口内,需要进行判定,hashmap1的数据在该窗格是否都存在 ---》hashmap2
             HashMap<String,Integer> hashmap2 = new HashMap<>();
            //  定义初始的word个数
            int num = 0;
            // 当word个数小于n时,窗格内要循环判断,终止条件时hashmap2的value大于hashmap1的value
            while(num < n){
                // 截取字符串,i相当于滑动偏移量,num*len和(num+1)*len 是窗格内每个单词截取的数据
                String str = s.substring( i + num * len,i + (num + 1) * len);
                // 储存到hashmap2
                hashmap2.put(str,hashmap2.getOrDefault(str,0)+1);
                // 若hashmap1中不包括str,说明该窗格不符合条件
                if(hashmap1.containsKey(str)){
                    // 接下来判断hashmap2中的value和hashmap1的value大小
                    if(hashmap2.get(str)>hashmap1.get(str)){
                        // 大于,则该窗格不符合(可画图验证)
                        break;
                    }
                    // 前进到下一个单词
                    num++;
                }else{
                    break;
                }
            }
            // 如果一个滑动窗口内所有单词都符合,那么添加i
            if(num==n){
                res.add(i);
            }
        }
    return res;

    }
}

复杂度分析

  • 时间复杂度:O(N^2)
  • 空间复杂度:O(N)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值