数据结构与算法笔记

数据结构与算法

0、做题宝典

1.做题逻辑

  1. 像人解题的思维去编程
  2. 再将这个思维用代码实现
  3. 先写出逻辑再Debug
  4. 最厚再加上各种特殊情况(完善健壮性)

1.先按照感觉写大致的逻辑
2.微调判断边界
3.加上特殊条件
4.让逻辑和代码变得更优雅

//当使用到循环处理时,先找出终止条件,再写出返回值

  1. 自己先做一遍 //简单方法
  2. 找一个题解,现学现卖 //优美方法

2.常用函数模板(建议记忆)

1.各个位数之和

计算一个多位整数的各位数字之和

int digitSum(int num){
    int sum = 0;
    while (num > 0){
        sum += num % 10;  //从尾部逐个加加
        num /= 10;
    }return sum;
}

2.声明二维数组

以bool型二维数组为例:

bool** vi = (bool**)malloc(sizeof(bool*)*m);  //申请M行数组
for(int i = 0; i < m; i++){
    vi[i] = (bool*)calloc(n,sizeof(bool));  //牛逼,calloc可以初始化
}

malloc申请后空间的值是随机的,并没有进行初始化,
而calloc却在申请后,对空间逐一进行初始化,并设置值为0;

3.变量和一维数组做出参

int** levelOrder(struct TreeNode* root, int* returnSize, int** returnColumnSizes){
//其中将变量和一位数组按照地址传递处理
//int* returnSize  实则想传出的是一个值
//int** returnColumnSizes  实则想传出的是一个一维数组
}

下面我们来处理他们

  1. 对于变量的操作
    如果想让该值自加的话记得先括上括号
(*returnSize)++;
  1. 对于一维数组的操作
//先申请一维数组的空间
 *returnColumnSizes = (int*)malloc(sizeof(int)*2000);
 //给数组的每个元素赋值(记得先用括号括起来)
 (*returnColumnSizes)[i++] = num;

1、基础知识

0.参考博文

参考《王道_数据结构

1.数据结构:逻辑结构 / 物理结构

1.逻辑结构

逻辑结构是指数据对象中数据元素之间的相互关系
可具体分为以下四种关系

A:集合结构

数据元素除了同属于一个集合外,它们之间没有其他关系
在这里插入图片描述

B:线性结构

数据元素之间是一对一关系
在这里插入图片描述

C:树形结构

数据元素之间呈现一对多关系
在这里插入图片描述

D:图形结构

数据元素是多对多关系
在这里插入图片描述

2.物理结构

是指数据在内存中真实存放的结构

1.顺序结构
2.链式结构

2、基本数据结构/C实现

0. 基本数据结构

  1. 链表 双向链表
  2. 树 二叉树 二叉搜索树 最小生成树
  3. 栈 单调栈
  4. 队列 单调队列
  5. 堆(优先队列)
  6. 哈希表

  7. 有序集合
    拓扑排序
    最短路
    欧拉回路
    双连通分量
    强连通分量

1. 代码实现

1. 链表

一串链表是一串结构体,可以拿一个结构体数组实现;

1. 链表结构体
struct ListNode{
	int val;				//数据域
	struct ListNode *next;  //指针域
	}
1.1(结构体来实现)链式结构

C 代码实现 参考

//================线性表的单链表存储结构=================
typedef struct LNode {
ElemType data;//数据域
struct LNode *next;//指针域(运用了结构体嵌套)
}LNode,*LinkList;  //给结构体起了两个别名,分别以:LNode* p\LinkList p的方式来声明结构体指针。

LNode* p = (LNode*)malloc(sizeof(LNode));//给节点分配内存。
free(p);  //释放链表

1.2删除链表
  1. 删除某个链表元素
    在这里插入图片描述
  2. 删除整个链表
void Clear_LinkList(struct LinkNode* head)
{
	if (head == NULL) return;
	//先清空链表,是不清空头节点的,因此从第一个有数据的节点开始释放
	struct LinkNode* curNode = head->next;
	while (curNode != NULL)
	{
		//先保住下一个节点的位置
		struct LinkNode* nextNode = curNode->next;
		free(curNode);
		curNode = nextNode;	
	}
	//释放头结点
	free(head);      //先还是后释放head都可
	//头结点指针置空
	head = NULL;
}

2. 树

0. 基本概念
  1. 满二叉树
    一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,深度为k(k>=1)且有(2^k)-1个结点的二叉树就是满二叉树
    在这里插入图片描述

  2. 完全二叉树
    若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
    在这里插入图片描述

  3. 最大堆
    什么是最大堆和最小堆?最大(小)堆是指在树中,存在一个结点而且该结点有儿子结点,该结点的data域值都不小于(大于)其儿子结点的data域值,并且它是一个完全二叉树(不是满二叉树)。
    在这里插入图片描述

1. 树的结构体
struct TreeNode {
    int val;				//数据域
    struct TreeNode *left;  //左子树  (运用了结构体嵌套)
    struct TreeNode *right; //右子树  (运用了结构体嵌套)
  };
1.1(链式结构)//没啥意义呀?

C代码实现 参考

#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
typedef int elemtype;
typedef struct tNode* tree;
typedef struct tNode {
 elemtype elem;
 tree left;  //左子树的结构体
 tree right; //右子树的结构体
}tNode;

计算树的节点个数

//明确函数的功能:返回传入树的节点个数
//定好尾头:尾:当传入的节点尾NULL时 头:1 + count(t->left) + count(t->right)
int count(tree t)   //tree等价于struct TreeNode*
{
 if (t == NULL) return 0;
 return 1 + count(t->left) + count(t->right);
}

3. 栈(stack)

一个数组栈是一个结构体内包函的数组在增加;
一个链栈是一串结构体;

1. 栈结构体
/*1.数组实现*/
typedef struct stack{
    int data[MAXSIZE];
    int top;	//用于栈顶指针
}Stack;
/*2.链表实现*/
typedef struct stack{
    int data;
    struct stack *next;
}Stack;
1.1(数组实现)
  1. C实现 参考
    上述不太好看懂
    请看下面的方式:参考CSDN
  2. 栈的结构定义
typedef struct
{
    int data[MAXSIZE];
    int top;	//用于栈顶指针
}Stack;
  1. 进栈操作push
/* 插入元素e为新的栈顶元素 */
Stack Push(Stack *S, SElemType e)
{
    if(S->top == MAXSIZE-1)	//栈满
        return ERROR;
    S->top++;	//栈顶指针+1
    S->data[S->top] = e;
    return OK;
}

  1. 出栈操作pop
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR */
Stack Pop(Stack *S, SElemType *e)
{
    if(S->top == -1){
        return ERROR;
    }
    *e = S->data[S->top];	//将要删除的栈顶元素赋值给e
    S->top--;	//栈顶指针-1
    return OK;
}

  1. 释放内存(自己写的)

这样写不一定对,要分数组是malloc申请的还是直接声明的(例如:int a[100])
他是malloc申请的数组,销毁的时候,可以直接这样吧,
一般数组栈有一个top指针,,直接置为-1就行了

free(data);
data = NULL;
1.2(链表实现)
  • 采用向链表头插元素的方式入栈
    在这里插入图片描述
#include<stdbool.h>
#include<stdlib.h>
typedef int datatype;    //便于修改栈内存储的数据类型

typedef struct stack{
    datatype data;
    struct stack *next;
}Stack;

Stack *s;  //创建栈

void init(){   //初始化栈
    s=NULL;
}
 
//判断栈是否为空
bool Empty()
{
    if(s==NULL)
    {
        return true;
    }
    else
    {
        return false;
    }
}
 
//判断栈是否已满了
// void full(Stack *s)
// {
//     if(s->top==realsize-1)
//     {
//         realsize++;
//         s->data=(datatype *)realloc(s->data,realsize);
//     }
// }
 
//入栈
void Push(datatype element)
{
    Stack *p = (Stack *)malloc(sizeof(Stack));
    p->data=element;
    p->next=s;   //头插
    s=p;             
}
 
//出栈
void Pop()
{
    if(!Empty(s))
    {
        s=s->next;
    }
    else
    {
        printf("栈空\n");
    }
}
 
//取栈顶元素
datatype Top()
{
    if(!Empty(s))
    {
        return s->data;
    }
    else
    {
        printf("栈空\n");
    }
}
 
//销毁栈
void Destroy()
{
    free(s);//应该销毁每一个元素   (元素全部出栈了才能只free头结点,不然的话要一个个先把元素都释放)
    s=NULL;
    // free(s.data); //容易导致失败
}

上述直接free头结点的方式销毁栈好像可行。

1.3 (两栈共享空间)

参考《大话数据结构》

  1. 问题引出
    为了解决栈空间不够用的问题,一般情况下数组栈都是事先定好数组空间,万一不够用了就需要编程手段来扩展数组容量,非常麻烦。

  2. 解决办法
    用一个数组来存储两个栈
    在这里插入图片描述

  3. 代码实现

/*两栈共享空间结构*/
typedef struct{
	SElemType data[MAXSIZE];
	int top1;  //栈1栈顶指针
	int top2;  //栈2栈顶指针
}Stack;

/*插入元素e为新的栈顶元素*/
Status Push (Stack *S, SElemType e, int StackNumber){
	if(S->top1 + 1 == S->top2)  //栈已满,不能再push新元素
		return error;
	if(StackNumber == 1)
		S->data[++S->top1] = e;  //若栈1则给栈1入栈
		else if(StackNumber == 2)
			S->data[--S->top2] = e;
		return OK;
}

/*选择栈完成元素出栈*/
Status Pop (Stack *S, SElemType *e, int StackNumber){
	if(StackNumber == 1){
		if(S->top1 == -1)
			return ERROR;   //栈1空
			*e = S->data[S->top1--];//栈1出栈
	}
	else if(StackNumber == 2){
		if(S->top2 == MAXSIZE)
			return ERROR;   //栈2空
			*e = S->data[S->top2++];//栈2出栈
	}
	return OK;
}

在这里插入图片描述

4. 队列(Queue)

1.(链式结构)

一串结构体相连形成队列本质上还是用链表
C语言代码实现 参考
Queue.h 文件:
1、定义结构体结构

//创建队列结构
typedef int QDataType; //方便后续更改存储数据类型,本文以int为例
//创建队列节点
typedef struct QueueNode
{
	QDataType data;           //存储数据
	struct QueueNode* next;   //记录下一个节点
}QNode;
//保存队头和队尾

2、队列头尾指针的结构体

typedef struct Queue
{
	QNode* head;   //头指针
	QNode* tail;   //尾指针
}Queue;
//对于求树的最大深度问题

3、数据入队列

/*
 * Data queuing
 */
void enqueue(QUEUE_TYPE value) {
	QueueNode *new_node;
	new_node = (QueueNode *)malloc(sizeof(QueueNode));
	if (new_node == NULL)
		perror("malloc fail\n");
	new_node->data = value;
	if (head == NULL) {
		head = new_node;
		tail = new_node;
	} else {
		tail->next = new_node;
		tail = new_node;
	}
}

4、数据出队列

/*
 * Data out of queue
 */
void dequeue(void) {
	assert(!is_empty());
	QueueNode *front_node;
	front_node = head;
	head = front_node->next;
	free(front_node);
	if (head == NULL)
		tail = NULL;
}

2. 两个栈实现队列

1.的特点是先进后出队列的特点是先进先出,这里我们可以使用两个栈(栈A和栈B)模拟队列
在这里插入图片描述
栈B为辅助栈
2. 弹出队首的数据,实际就是删掉栈底的数据,直接删除不了,可以利用另一个辅助栈,将当前栈中数据全部依次弹出,并push到辅助栈中,这时当前栈的栈底元素就变成了辅助栈的栈顶元素,直接pop掉辅助栈的栈顶元素就行。
3. 队列从队首到队尾的元素始终是辅助栈栈顶到栈底+当前栈栈底到栈顶。辅助栈栈顶对应着队列队首,当前栈栈底对应队列队尾。这就说明当辅助栈不为空时,队列的pop,可以直接对辅助栈pop当辅助栈为空时,就要将当前栈搬运到辅助栈再弹出。而入栈可以直接push当当前栈。

  • C语言实现

typedef struct{
	int* data;
	int top;
}MyStack;

typedef struct{
	MyStack* stackPush;   //输入栈
	MyStack* stackPop;    //输出栈(辅助栈)
}MyQueue;

#define maxQueueNum  100		//队列空间
MyQueue* myQueueCreate(){
	MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue));
	
	queue->stackPush = (MyStack*)malloc(sizeof(MyStack));
	queue->stackPop = (MyStack*)malloc(sizeof(MyStack));
	
	queue->stackPush->data = (int*)malloc(sizeof(int) * maxQueueNum);
	queue->stackPush->top = -1;
	
	queue->stackPop->data = (int*)malloc(sizeof(int) * maxQueueNum);
	queue->stackPop->top = -1;
	
	return queue;
}

//入队
void myQueuePush(MyQueue* obj){
	if((obj->stackPush->top + obj->stackPop->top) == maxStackNum - 2)	//队列空间满
		return ;
	obj->stackPush->data[++obj->stackPush->top] = x;
}

//出队
int myQueuePop(MyQueue* obj){
	if(obj->stackPop->top != -1){		//辅助栈不为空,直接弹出
		return obj->stackPop->data[obj->stackPop->top--];
	}
	while(obj->stackPush->top != -1){	//辅助栈为空,将当前栈依次弹出并压到辅助栈中
		obj->stackPop->data[++obj->stackPop->top] = obj->stackPush->data[obj->stackPush->top--];
	}
	if(obj->stackPop->top == -1){		//辅助栈仍为空,说明对应队列为空,无值返回
		return -1;
	}
	else
	{
		return obj->stackPop->data[obj->stckPop->top--];
	}
}

//取队首值
int myQueuePeek(MyQueue* obj){
	if((obj->stackPush->top == -1) && (obj->stackPop->top == -1))//队列为空
		return -1if(obj->stackPop->top != -1){
		return obj->stackPop->data[obj->stackPop->top];
	}
	else
	{
		while(obj->stackPush->top != -1){
			obj->stackPop->data[++obj->stackPop->top] = obj->stackPush->data[obj->stackPush->top--];
		}
		return obj->stackPop->data[obj->stackPop->top];
	}
}

//判断队列是否为空
bool myQueueEmpty(MyQueue* obj){
	if((obj->stackPush->top == -1) && (obj->stackPop->top == -1))
		return true;
	else
		return false;
}

void myQueueFree(MyQueue* obj) {
    free(obj->stackPush->data);
    free(obj->stackPop->data);
    free(obj->stackPush);
    free(obj->stackPop);
    free(obj);
}


  • C++实现
class CQueue {
private:
    stack<int> inStack, outStack;  //创建了两个栈

    void in2out() {             
        while (!inStack.empty()) {
            outStack.push(inStack.top());
            inStack.pop();
        }
    }

public:
    CQueue() {}

    void appendTail(int value) {
        inStack.push(value);
    }

    int deleteHead() {
        if (outStack.empty()) {
            if (inStack.empty()) {
                return -1;
            }
            in2out();
        }
        int value = outStack.top();
        outStack.pop();
        return value;
    }
};

5.DFS/BFS

深度优先级搜索/广度优先搜索

(DFS,Depth-First Search)
(BFS,Breath-First Search)

DFS:在树中,用递归的方式,寻找一个深度最大的路径
BFS:在树中,将树一层一层拼接为一个队列,看有几层

深度优先搜索相当于对搜索树进行前序遍历,而广度优先搜索则相当于层序遍历

6. C言语哈希表(uthash)使用

参考CSDN
哈希函数形象比喻:
关键字(key)—— 哈希函数(f()) —— 哈希值
例如要在字典上查“按”这个字
在这里插入图片描述
按就是关键字(key),f()就是字典索引,也就是哈希函数,查到的页码4就是哈希值。

由于C语言本身不存在哈希,但是当需要使用哈希表的时候自己构建哈希会异常复杂。因此,我们可以调用开源的第三方头文件,这只是一个头文件:uthash.h。我们需要做的就是将头文件复制到您的项目中,然后:#include “uthash.h”。

uthash还包括三个额外的头文件,主要提供链表,动态数组和字符串。utlist.h为C结构提供了链接列表宏。utarray.h使用宏实现动态数组。utstring.h实现基本的动态字符串。
  github下载链接:https://github.com/troydhanson/uthash

一、 uthash的使用
以构建一个姓名和学号的哈希表为例

  1. 第一步:包含头文件
#include "uthash.h"
  1. 第二步:自定义数据结构。每个结构代表一个键-值对,并且,每个结构中要有一个UT_hash_handle成员。
  struct my_struct {
int id; /* key */
char name[10];
UT_hash_handle hh; /* hh是内部使用的hash处理句柄,不需要为该句柄变量赋值,但必须在该结构体中定义该变量。 */
};
  1. 第三步:定义hash表指针。这个指针为前面自定义数据结构的指针,并初始化为NULL。
struct my_struct *users = NULL; /* important! initialize to NULL */
  1. 第四步:进行一般的操作。

增加:

  int add_user(int user_id, char *name) {
struct my_struct *s;
s = malloc(sizeof(struct my_struct));
s->id = user_id;
strcpy(s->name, name);
HASH_ADD_INT( users, id, s ); /* id: name of key field */
}
  • HASH_ADD_INT函数中,第一个参数users是哈希表,第二个参数id是键字段的名称。最后一个参数s是指向要添加的结构的指针。
    查找:
  struct my_struct *find_user(int user_id) {
struct my_struct *s;
HASH_FIND_INT( users, &user_id, s ); /* s: output pointer */
return s;
}
  • 在上述代码中,第一个参数users是哈希表,第二个参数是user_id的地址(一定要传递地址)。最后s是输出变量。当可以在哈希表中找到相应键值时,s返回给定键的结构,当找不到时s返回NULL。
    删除:
  void delete_user(struct my_struct *user) {
HASH_DEL( users, user); /* user: pointer to deletee */
free(user); /* optional; it’s up to you! */
}

计数:

  unsigned int num_users;
num_users = HASH_COUNT(users);
printf("there are %u users/n", num_users);

迭代:

  void print_users() {
struct my_struct *s;
for(s=users; s != NULL; s=s->hh.next) {
printf("user id %d: name %s/n", s->id, s->name
}
}

排序:

  int name_sort(struct my_struct *a, struct my_struct *b) {
return strcmp(a->name,b->name);
}
int id_sort(struct my_struct *a, struct my_struct *b) {
return (a->id - b->id);
}
void sort_by_name() {
HASH_SORT(users, name_sort);
}
void sort_by_id() {
HASH_SORT(users, id_sort);
}

要注意,在uthash中,并不会对其所储存的值进行移动或者复制,也不会进行内存的释放。

3、刷题实践

输入输出——————————————

1. 利用scanf输入两行数字:

  • scanf输入不定长数组:
    int a[100]={0};
    int length=0;
do{
    scanf("%d",&a[length++]);
}while(getchar()!='\n');         //scanf输入不定长数组
    for(int i=0;i<length;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
    printf("%d \n",length);
    return 0;
}

2. 多行输入

题目描述:
求a + b

输入描述:
输入包括两个正整数a,b(1 <= a, b <= 1000),输入数据包括多组(多组a和b)。
输出描述:
输出a+b的结果

#include <stdio.h>

int main(){
    int a,b;
    while(scanf("%d %d", &a, &b) != EOF){
        printf("%d\n", a+b);
    }
    return 0;
}

3. 二维数组输入多个字符串

  1. 利用scanf输入字符串:
    char a[10] = {0};
    scanf("%s",a);
  1. 题目描述
    对输入的字符串进行排序后输出

输入描述:
输入有两行,第一行n

第二行是n个字符串,字符串之间用空格隔开
输出描述:
输出一行排序后的字符串,空格隔开,无结尾空格

    int n, a, b;
    scanf("%d", &n);
    char arr[n][100];
    for(int i = 0; i < n; i++){
        scanf("%s", arr[i]);
    }
    
    qsort(arr, n, sizeof(arr[0]), strcmp);  //用到了字符串比较函数作为快排函数
    for(int i = 0; i < n-1; i++){
        printf("%s ", arr[i]);
    }
    printf("%s", arr[n-1]);
    return 0;
}
  1. 二维字符串数组的问题
    char a[2][10];
    a[0] = "shhajsa";

这是错误的
原因:

(1)数组不能直接给数组赋值
(2)指针不能直接给数组赋值

//前提条件
char a[] = {'a','b','c'};
char b[3];

char  c[3]  = a;	//错误---》数组不能直接给数组赋值
char  d[3] 	= p;	//错误---》指针不能直接给数组赋值
char *p  = a;	//正确赋值,数组名为数组首元素地址,因此可使用指针保存该地址(指针只能保存地址)
strcpy(b,a);	//正确,使用标准库函数可实现字符串拷贝
char **p1 = &p;	//正确,二级指针可接收一级指针的地址

正确做法:

char a[2][10] = {"shhajsa","hhahjha"};

4. 二维数组输入多行数字

  1. 输入样例
    2 5 //行数和列数
    1 4 6 23 1
    1 -5 2 4 6
  2. 代码实现
int main(void)
{
	int a[101][101],m,n,i,j;
	scanf("%d%d",&m,&n);
	for(i=0;i<m;i++){
		for(j=0;j<n;j++){
			scanf("%d",&a[i][j]);
		}
	}
	//输出
	for(i=0;i<m;i++){
		for(j=0;j<n;j++){
			printf("%d ",a[i][j]);
			}
		printf("\n");
	}
	return 0;
 } 

数据结构——————————————

1. 链表

1.链表结构体

struct ListNode{
	int val;				//数据域
	struct ListNode *next;  //指针域
	}
  1. F和Q分别是指向单链表两个元素的指针,那么,F所指元素是Q所指元素后继的条件是(Q->next == F)

1.力扣链表题

1.剑指 Offer 06. 从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* reversePrint(struct ListNode* head, int* returnSize){
    struct ListNode* p = head;
    int i = 0;
    static int arry[10000];
    //if(head == NULL) return NULL;
    while(p != NULL)
    {
        i++;
        p = p -> next;
    }
    *returnSize = i;
    p = head;
    while(p != NULL)
    {
         arry[--i] = p -> val;
        p = p -> next;
    }
    return arry;
}
  • 笔记

Line 207: Char 3: runtime error: load of null pointer of type ‘int’ [Serializer.c]
得出这是因为你在函数里设置的是局部变量数组,当退出该函数,该变量数组也会随之销毁。当指针再回来找该变量是找不到的,你return也return了个寂寞

局部变量不能return,用static关键字修饰一下就好了。

2.剑指 Offer 18. 删除链表的节点

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* deleteNode(struct ListNode* head, int val){
   //struct ListNode* h = (struct ListNode*)malloc(sizeof(struct ListNode));
    //h->next = head;       //这是给无头结点链表创建有个头结点的规范做法,本题用不到。
    struct ListNode* p = head;
    if(p->val == val)
    {
        return head->next;
    }
    while(p != NULL)
    {
        if(p->next->val == val) break;
        p = p->next;
    }
    p->next = p->next->next;
    return head;
}
  • 心得
  1. 题目中的链表一般是不带头节点的链表
  2. 也就是head节点直接具有自己的val和next
  3. 如果想要自己创建一个头结点(只有next,val为空)
  4. 则需要用malloc(因为要创建的头结点是一个实体,需要先给它分配空间,不像p只是一个结构体指针,只需要指向链表的各个节点就好了)
  • 方法二、有头结点
struct ListNode* deleteNode(struct ListNode* head, int val){

struct ListNode* cur = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* ans = cur;//以便返回更改后的链表
cur->next = head;

while(cur->next != NULL && cur->next->val != val){
    cur = cur->next;
}

if(cur->next != NULL){//找到目标节点
    cur->next = cur->next->next;//执行删除操作
    return ans->next;//返回新链表
}

return NULL;
}
3.剑指 Offer 22. 链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* getKthFromEnd(struct ListNode* head, int k){
    //1、简单做法
   /*
    struct ListNode* p = head;  //遍历指针
    struct ListNode* n = head;  //返回指针
    int i = 1;
    if(head == NULL) return NULL;
    while(p->next != NULL)      //计数循环
    {
        p = p->next;
        i++;
    }
    for(int j=0; j < (i-k); j++)  //找出倒数第k个节点
    {
        n = n->next;
    }
    return n;
    */
    //2、优雅方法(快慢指针)
    struct ListNode*p = head,*slow_p = head;
    while(--k) p = p->next;
    while(p->next != NULL)
    {
        p = p->next;
        slow_p = slow_p->next;
    }
    return slow_p;
}
4.剑指 Offer 24. 反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

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

限制:0 <= 节点个数 <= 5000

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* reverseList(struct ListNode* head){
    //简单方法(暴力解法)
    /*
    struct ListNode* p = head;
    struct ListNode* n = head;
    int a[5000];
    int i = 0;
    while(p != NULL)   //将链表各个节点放入数组
    {
        a[i++] = p->val;
        if(p->next != NULL)
        p = p->next;
        else break;
    }
    for(int j = 1; j <= i; j++)
    {
        n->val = a[i-j];
        n = n->next;
    }
    return head;
    */
    //优雅做法(三指针)  回头再啃一遍
    struct ListNode* prev = NULL;
    struct ListNode* curr = head;
    while (curr) {
        struct ListNode* next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}
  • 感悟:
  • 指针 = 指针
    左值是指针,右值是地址,左边的指针指向右边指针所指的地址。
  • curr->next = prev;
    左值是指针,右值是地址,左边的指针指向右边指针所指的地址。

3.经验总结

2. 树

树的各种品种,参考CSDN

1.树的结构体

struct TreeNode {
    int val;				//数据域
    struct TreeNode *left;  //左子树  (运用了结构体嵌套)
    struct TreeNode *right; //右子树  (运用了结构体嵌套)
  };

2.二叉树

1. 题目引入
  • 已知二叉树后序遍历序列为dabec,中序遍历序列为debac,它的前序遍历序列为

答案: cedba
在这里插入图片描述

2. 前/中/后/层 序遍历

前/中/后/层 序遍历

  • 递归实现前/中/后/层 序遍历
    参考CSDN
  1. 前序遍历:根节点 | 左子树 | 右子树
  2. 中序遍历:左子树 | 根节点 | 右子树
  3. 后序遍历:左子树 | 右子树 | 根节点
  4. 后序遍历:一层一层遍历

在这里插入图片描述

  • 前序遍历: 1 2 4 5 3 6 7

  • 中序遍历: 4 2 5 1 6 3 7

  • 后序遍历: 4 5 2 6 7 3 1

  • 层序遍历: 1 2 3 4 5 6 7

3. 树的前、中、后 / 层序遍历代码实现

1. 前、中、后序遍历中序
  • 输出一个二叉树
  • 输入:二叉树
  • 输出:二叉树的中序数组和数组元素个数;
#include <stdio.h>
#include<stdlib.h>
	//int * a = (int*)malloc(sizeof(int)*1000);
	int a[1000] = {0};  //在C标准中全局变量要初始化
	static int i = 0;   //全局变量最好拿static修饰,防止和其他文件变量重名

struct tree {
    int val;
    struct tree* left;
    struct tree* right;
};

struct tree* digui(struct tree* root){  //递归遍历函数
	if(root != NULL){
	digui(root->left);
	a[i++] = root->val;          //这一行是关键,前中后序分别放在前中后
	digui(root->right);
	}
	return root;
}
int* zhongxu(struct tree* root, int* numSize){  //整合函数

	digui(root);
	*numSize = i;
	return a;
} //到这里答题就算完成了——————————————————————————————————————————————————————————————————
int main(void){     //创建一个小树,测试输出
	printf("进入main\n");
    struct ListNode* root1 = (struct ListNode*)malloc(sizeof(struct ListNode));
    root1->val = 5;
	root1->left = NULL;
	root1->right = NULL;
    int *n = (int*)malloc(sizeof(int));  //声明指针就要分配空间,不要存在空指针。
    int *b = zhongxu(root1,n);
	for(int j = 0; j<i; j++)
		printf("该树的中序遍历为:{ %d }\n",b[j]);
	printf("numSize = %d\n",*n);
}
2. 层序遍历
  • 层序遍历
    //先记录当前节点的值,然后再遍历左孩子,直到当前节点没有左孩子则遍历其右孩子
    //若右孩子也没有则回溯到上一个节点遍历其右孩子,直至遍历完树
    //时间复杂度O(N), 空间复杂度O(N)

原理:
一个结构体数组形成的队列动态放置树的节点的层序
一个结构体指针不断的往这个队列中放入节点
一个数组不断从队列中取节点的值

int* levelOrder(struct TreeNode* root, int* returnSize){
    if(root == NULL){
        (*returnSize)= 0;
        return root;
    }

    struct TreeNode* queue[1001];
    int front = 0, rear = 0, i = 0;
    struct TreeNode* p;
    int* array = (int*)malloc(sizeof(int)*1001);

    queue[rear++] = root;
    while(front < rear){
        p = queue[front++];
        array[i++] = p->val;
        if(p->left)
            queue[rear++] = p->left;
        if(p->right)
            queue[rear++] = p->right;
    }
        *returnSize = i;

    return array;
}   

3. 堆/栈

算法—————————————————

4、排序

4.1、C库函数

0.qsort():快速排序

参考CSDN

  1. 函数原型
#iclude<stdlib.h>
void qsort(void*, size_t, size_t, int(*)(const void*, const void*))

第一个参数为待排序数组首地址。
可直接输入待排序数组名,或是指向数组的指针。
第二个参数为数组长度。
size_t是标准C库中定义的,应为unsigned int,在64位系统中为 long unsigned int。
可以直接输入待排序的数组的长度。
第三个参数为数组元素所占字节。
可直接用sizeof(a[0])计算字数组单个元素的节数。
第四个参数为所调用函数的指针,函数名即是函数的指针,可直接写函数名,调用函数用来确定排序的方式。

  1. 第四个参数(传一个函数)
    先以整型递增排序为例
int inc(const void*a, const void*b){
	return *(int*)a - *(int*)b; 
	}
  1. int inc 表示函数返回一个int值。inc为函数名 ,表示递增排序(increase),也可以自己命名。
  2. ( const void * a, const void * b)将两个要对比的元素的地址传入函数。 (加const表示无法改变指针指向的值)
  3. return * ( int * )a - * ( int * ) b ,返回一个整型数,表示两个元素对比的结果。如果a大于b,则返回正数。a小于b,则返回负数。如果a等于b,则返回零。(int *)表示将地址强制类型转换成整形地址类型,可根据排序对象选择指针类型的转换。也可以改变算式,例如用 return strlen((char * )a) > strlen((char * )b) ? 1 : -1; 可以返回比较字符串长度的结果,用来根据字符串长度进行排序, 下面有相关的代码。
  1. 各种数据类型的升序排序函数
  • 如果要降序排序,只需将return里的a,b反过来写即可。
    1、整型
int inc (const void * a,const void *b)
 {
return *(int *)a - *(int *)b;
}

2、double型

int inc (const void * a, const void * b)
{
return *(double *)a > *(double *)b ? 1 : -1;
}
注: 这里两个浮点数相减但要返回一个整型数,如果按上面做法直接减会丢失小数点部分。所以需另加处理,直接判断大小,如果a大于b,则返回1,否则返回-1

3、字符排序

int inc(const void *a,const void *b)
{
   return *(char *)a - *(char *)b;
}

  1. 力扣例题
    剑指 Offer 39. 数组中出现次数超过一半的数字
int increase(const void* a, const void* b){  //直接写(int* a, int* b)也可
    return *(int*)a - *(int*)b;  //都去掉(int*)也可
}
int majorityElement(int* nums, int numsSize){
    qsort(nums, numsSize, sizeof(int), increase);  //采用C库函数,快速排序
    return nums[numsSize / 2];
}
  1. 对结构体数组进行排序
    以结构体的val进行排序
int cmp(const void *a, const void *b){
    return (*((hash*)a)).val - (*((hash*)b)).val;      //直接取值用结构体变量也可以进行值的访问
}

调用:

 qsort(H, arrSize, sizeof(hash), cmp);

4.2、C语言实现各种排序

参考CSDN
C语言八大排序算法CSDN

0. 各排序算法复杂度

(时间复杂度)
在这里插入图片描述

方式: 平均——最坏——最好
插入: n^2 —— n^2——n
希尔: n^1.3—— /—— /
冒泡 :n^2 ——n^2 ——n
快速 :nlogn—— n^2 ——nlogn
选择: n^2—— n^2—— n^2
堆排: nlogn—— nlogn ——nlogn
归并: nlogn ——nlogn ——nlogn
基数: d(n+r) ——d(n+r)—— d(n+r)
其中平均和最坏均为nlogn的有堆排 和 归并

1. 冒泡排序 O(n^2)

循环比较两个元素,较大值(往后移)冒出来,最后移到最后一个,下一次再循环比较,最后移到倒数第二个;循环结束后,数组有序.

  • 法1:(冒泡排序优化版)
/*冒泡排序优化版*/
void SelectSort(int arr[],size_t len){
for(int i=0;i < len - 1; i++){
	for(int j=i; j<=len - 1; j++){
		if(arr[j]<arr[i])
			int temp = arr[i];
			arr[i] = arr[j];
			arr[j] = temp;
	}
  }
}

  • 法2:(正宗冒泡排序)
    相邻两个元素比较,按顺序交换位置,直到将最大值移动到最后。
void swap(int* a, int* b){
	int temp = *a;
	*a = *b;
	*b = temp;
	}
int SelectSort(int *s, size_t len){
	int i = 0, j = len - 1;
	for; j > 0; j--{
	while(i < j){
	if(s[i] > s[i+1]) swap(&s[i], &s[i+1]);
	i++;
	}
 }
 return 0;
}

2. 选择排序 O(n^2)

遍历数组 能够记录 最大值的下标
把最大值 和 最后一个元素 交换
遍历第二遍(不需要最后一个)数组 又能记录最大值的下标
把最大值 和 倒数第二个元素进行交换循环 len-1 次

//正宗选择排序(从小到大排序)
void SelectSort(int R[], int n)
{
    int i, j, min, tmp;
    for (i = 0; i < n - 1; i++)
    {
        min = i;
        for (j = i; j < n - 1; j++)/*比较完成再交换值*/
        {
            if (R[j] < R[min])
                min = j;
        }
        if (min != i)  //当最小值不在该位置时
        {
            tmp = R[min];
            R[min] = R[i];
            R[i] = tmp;
        }
    }
}

3.快速排序 O(nlogn)

B站视频

记下左右两端的数序号[left,right],记录一个数比如说 data = arr[left] 序号
然后去left右边 找一个比 arr[left] 小的数放在左边
后又去right左边找一个比 arr[left] 大的数放在右边
循环直到最后
递归调用自己 执行每一个数

void quick(int arr[],int left,int right){
	int data = arr[left];
	int i = left;
	int j = right;
	while(i<j){
		while(i<j&&arr[j]>data){--j;}	//查找左边的小于等于data的数
		arr[i] = arr[j];				//放置在左边i
		while(i<j&&arr[i]<data){++i;}	//查找右边大于等于data的数
		arr[j] = arr[i];				//放置在右边j
	}
	arr[i] = data;
	if(i-left>1){				//假如左边的数个数大于1,再次递归循环
		quick(arr,left,i-1);
	}
	if(right-i>1){				//假如右边的数的个数>1,再次递归循环
		quick(arr,i+1,right);
	}
}
void quickSort(int arr[],size_t len){
	quick(arr,0,len-1);
}

4. 堆排序 O(nlogn)

面试题40. 最小的k个数
参考bilibili
参考CSDN

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或大于)它的父节点。

一、数组转换二叉树,从上往下、从左往右对应数组下标,则对于任意结点下标k

  1. 其父结点下标 =【(k - 1)/ 2】
  2. 左孩子下标 =【2k + 1】
  3. 右孩子下标 =【2k + 2】
    例如:
    在这里插入图片描述
  • 堆排序步骤:
  1. 最大堆调整(Heapify)
    该步可以用一个函数实现局部的子树的最大堆;
  2. 创建最大堆(CreateHeap)
    该步借助树节点元素的变换,调用Heapify使得整个树变成一个最大堆(父节点都比子节点大);
  3. 堆排序(HeapSort)
    你可能会疑问既然是最大堆,那直接层序遍历输出不就好了,其实,推排序只是借助树和堆的思想,本质还是拿数组实现,所以没法层序遍历输出,还是得老老实实通过下标交换输出。
  • 代码实现
    一共四个函数:
    元素交换(Swap)
    最大堆调整(Heapify)
    创建最大堆(CreateHeap)
    堆排序(HeapSort)//还不如拿一个数组去接收最大堆的数组的倒序呢(想太少了,最大堆又不是从大到小以此排序的)
#include <stdio.h>
#define size 10

void Swap(int *num, int i, int j)
{
    int temp;
    temp = num[i];
    num[i] = num[j];
    num[j] = temp;
}

// 最大堆调整
void Heapify(int *num, int len, int k)
{
    if (k < len)
    {
        int root = k;           // 根结点
        int lchild = 2*k + 1;   // 左孩子结点
        int rchild = 2*k + 2;   // 右孩子结点
        // 查找左右孩子结点中的最大结点
        if (lchild < len && num[root] < num[lchild])
        {
            root = lchild; 
        }
        if (rchild < len && num[root] < num[rchild])
        {
            root = rchild;
        }
        
        // 交换最大结点到根结点
        if (root != k)
        {
            Swap(num, root, k);
            // 每次交换都可能影响到对应孩子结点子树的顺序
            // 所以需要对交换后的孩子结点子树进行最大堆调整
            Heapify(num, len, root);
        }
    }
}

// 创建最大堆
void CreateHeap(int *num, int len)
{
    int i;
    // 最后一个结点下标
    int last = len - 1;   
    // 最后一个结点的父结点下标      
    int parent = (last - 1) / 2;
    // 从最后一个结点的父结点到根结点,依次进行最大堆调整
    for (i = parent; i >= 0; i--)
    {
        Heapify(num, len, i);
    }
}

// 堆排序
void HeapSort(int *num, int len)
{
    // 创建最大堆并进行最大堆调整
    CreateHeap(num, len);
    printf("最大堆调整!\n");
    int i;
    // 依次取出根结点(最大值)
    for (i = len - 1; i >= 0; i--)
    {
        // 将最大堆的根结点(最大值)换到最后一个结点
        Swap(num, i, 0);
        // 交换后二叉树的根结点发生了改变,故还需对根结点做最大堆调整(已交换的末尾结点不参与调整)
        // 而此时根结点小于所有父结点,(因而在调整时只需考虑最大孩子的分支即可)?对这句话保留意见
        Heapify(num, i, 0); 
    }   
}

int main()
{
    int i, num[size] = {8, 4, 3, 1, 6, 9, 5, 7, 2, 0};
    HeapSort(num, size);

    for (i = 0; i < size; i++)
    {
        printf("%d ", num[i]);
    }
    printf("\n");
    return 0;
}

4. 直接插入排序 O(n^2)

5. 二叉树 堆排序 O(nlogn)

6. 希尔插入排序 O(nlogn)

7. 归并排序 O(nlogn)

5、贪心算法

  • 例1见蔚来笔试准备篇
    给你一个 m 行 n 列的二维网格 grid 和一个整数 k。你需要将 grid 迁移 k 次。

每次「迁移」操作将会引发下述活动:

位于 grid[i][j] 的元素将会移动到 grid[i][j + 1]。
位于 grid[i][n - 1] 的元素将会移动到 grid[i + 1][0]。
位于 grid[m - 1][n - 1] 的元素将会移动到 grid[0][0]。
请你返回 k 次迁移操作后最终得到的 二维网格。

示例 1:
在这里插入图片描述

int** shiftGrid(int** grid, int gridSize, int* gridColSize, int k, int* returnSize, int** returnColumnSizes){
    int m = gridSize, n = gridColSize[0];
    *returnColumnSizes = (int*)malloc(sizeof(int)*m);//A:为这个二维数组对应的一维数组申请m行
    int **ret = (int**)malloc(sizeof(int*)*m);       //1.为二维数组申请m个一维数组空间
    for(int i = 0; i < m; i++){
        ret[i] = (int*)malloc(sizeof(int)*n);        //2.为每个一维数组申请空间
        (*returnColumnSizes)[i] = n;                 //B:为这个一维数组各个元素赋值?          
    }
    for(int i = 0; i < m; i++){
        for(int j = 0; j < n; j++){
            int index = (i*n+j+k)%(m*n);
            ret[index/n][index%n] = grid[i][j];
        }
    }
    *returnSize = m;
    return ret;
}

6、动态规划

动态规划中每一个状态一定是由上一个状态推导出来的

  1. 动态规划问题五步曲
    1) 确定dp数组(dp table)以及下标的含义
    2)确定递推公式
    3)dp数组如何初始化
    4)确定遍历顺序
    5)举例推导dp数组

  2. 青蛙跳台阶问题
    假设有N级台阶,一次跳两节或者一节

int numWays(int n){
    int arr[n+2];
    arr[0] = 1;
    arr[1] = 1;
    if(n > 1){
    for(int i = 2; i <= n; i++){
    //arr[i]代表取余结果;
        arr[i] = (arr[i-1] + arr[i-2])%1000000007;  //除以同一个数,和的余数=两个加数的余数之和的余数
    }
    }
    return arr[n];
}

7、dfs / bfs

(DFS,Depth-First Search)
(BFS,Breath-First Search)

深度优先级搜索/广度优先搜索
1. 深度优先级搜索
  1. 剑指 Offer 13. 机器人的运动范围
  2. 题目描述:
    地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

在这里插入图片描述

//官方解法,dfs深度优先搜索
// 计算各个位数之和
int digitSum(int num){
    int sum = 0;
    while (num > 0){
        sum += num % 10;
        num /= 10;
    }
    return sum;
}
// x,y 当前位置// m,n 当前矩阵长和宽// visited 访问标记// k 目标值
int dfsTraversal(int x, int y, int m, int n, bool **visited, int k)
{
 if (x < 0 || y < 0 || x >= m || y >= n || digitSum(x) + digitSum(y) > k || visited[x][y] == true)
        return 0;
 /* 标记当前节点已到达 */        
    visited[x][y] = true;
    return 1 + dfsTraversal(x, y + 1, m, n, visited, k) + dfsTraversal(x + 1, y, m, n, visited, k);
}
int movingCount(int m, int n, int k){ 
    bool **visited = (bool **)malloc(sizeof(bool *) * m);
    for (int i = 0; i < m; i++)
    {
        visited[i] = (bool *)calloc(n, sizeof(bool));
    }
    return dfsTraversal(0, 0, m, n, visited, k);
}

8、回溯

  1. 力扣原题

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。

  • 递归+for循环
    在这里插入图片描述
//代码随想录学习
int* path;
int pathTop;
int** ans;
int ansTop;

void backtracking(int n, int k, int startIndex){
//当path中元素个数为k个时,我们需要将path数组放入ans二维数组中
    if(pathTop == k){
     //path数组为我们动态申请,若直接将其地址放入二维数组,path数组中的值会随着我们回溯而逐渐变化
        //因此创建新的数组存储path中的值
        int* temp = (int*)malloc(sizeof(int) * k);
        for(int i = 0; i < k; i++){
            temp[i] = path[i];
        }
        ans[ansTop++] = temp;
        return;
    }
    //for循环来了!
    //剪枝操作 for(j = startIndex; j <= n- (k - pathTop) + 1;j++) 
    for(int j = startIndex; j <= n; j++){
    //将当前结点放入path数组
        path[pathTop++] = j;
        backtracking(n, k, j + 1);
        pathTop--;
    }
}

int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
    path = (int*)malloc(sizeof(int) * k);
    ans = (int**)malloc(sizeof(int*) * 10000);
    pathTop = ansTop = 0;
    //回溯算法
    backtracking(n, k, 1);
    *returnSize = ansTop;  //返回的二维数组的行数
    *returnColumnSizes = (int*)malloc(sizeof(int) * (*returnSize));  //返回的二维数组每行的列数
    for(int i = 0; i < *returnSize; i++){
        (*returnColumnSizes)[i] = k;
    }
    return ans;
}

9、查找

  1. 二分法查找
    用于已排序好的数组
/*
非递归二分法查找
srcArray:输入的数组
nSize:数组的元素个数
key:要查找的数
*/
int SearchBin(int *srcArray, int nSize, int key){
if(!srcArray || nSize == 0) return -1;
int l = 0;
int r = nSize - 1;
int mid = 0;
while(l <= r){
mid = (l + r)/2;
if(srcArray[mid] > key) r = mid;
else if(srcArray[mid] < key) l = mid;
else return mid;
}
return -1;
}

时间/空间复杂度

在这里插入图片描述

算法的真理

  • 流程控制语句(for、while)、条件判断语句(if、else)、递归 的排列组合;

何时可以用递归?
1.问题的解可以分为几个子问题的解。何为子问题?就是数据规模更小的问题。
2.问题与子问题,出来数据规模不同,求解思路完全一样;
3.存在递归终止条件;

20世纪10个最伟大的算法

参考CSDN
由Computer in Science &Enigeering和IEEE Computer Society联合评选出来的20世纪10个最伟大的算法:
1、蒙特卡罗算法。1946: John von Neumann, Stan Ulam, and Nick Metropolis
2、单纯形方法。1947: George Dantzig,学过运筹学的人都知道:)
3、Krylov 子空间迭代算法。1950: Magnus Hestenes, Eduard Stiefel, and Cornelius Lanczos。Krylov subspace:span{S,A*S,A2*S,…,A(k-1)*S}.
4、矩阵分解算法。1951: Alston Householder。
5、Fotran 最优化编译器。1957: John Backus。
6、QR算法。1959–61: J.G.F. Francis
7、快速排序算法 1962: Tony Hoare。你能在10分钟内写出个完全正确的版本么:)
8、FFT算法。1965: James Cooley
9、整数关系确定算法(Integer Relation Detecting Algorithms)。1977: Helaman Ferguson and Rodney Forcade。
10、快速多极算法(Fast Multipole Algorithms )。1987: Leslie Greengard and Vladimir Rokhlin。

机器学习十大算法

参考:知乎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值