1.数据结构绪论
1.1数据结构的基本概念和术语
1.数据:是描述客观事物的符号,是计算机中可操作的对象,是能被计算机识别,并输入处理给计算机处理的符号集合。一切都是数据。这里说的数据其实就是符号,这些符号必须具备两个前提:(1)可以输入到计算机中。(2)能被计算机程序处理。
2.数据元素:是组成数据的、有一定意义的单位,在计算机中通常作为整体处理,也被称为记录。比如人类的数据对象就是人。
3.数据项:一个数据元素可以由若干个数据项组成。数据项是数据不可分割的最小单位。比如人这样的数据元素,他的眼睛、手自雷的可以作为数据项。
4.数据对象:是性质相同的数据元素的集合,是数据的子集(表、实体、数据集)
5.数据结构:不同数据元素之间不是独立的,而是存在特定关系的,我们将这些关系称为结构。数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
1.2逻辑结构与物理结构
按照视点不同,将数据结构分为逻辑结构和物理结构
1.2.1逻辑结构
逻辑结构:是指数据对象中数据元素之间的相互关系。
逻辑结构分为4种:
(1)集合结构:集合结构中的元素除了同属于一个集合外,它们之间没有其他关系。如同数学中的集合。
(2)线性结构:线性结构中的数据是一对一的关系。可以理解为一串珠子。
(3)树形结构:树形结构中数据元素之间存在一种对多种的关系。
(4)图形结构:图形结构的数据元素是多对多的关系。
1.2.2物理结构(存储结构)
物理结构:是指数据的逻辑结构在计算机中的存储形式。
数据元素的存储结构有两种:顺序存储和链式存储。
(1)顺序存储结构:是把数据元素存放在连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。
(2)链式存储结构:是吧数据元素存放在任意的存储单元里,这组存储单元可以是连续的也可以是不连续的。用一个指针存放数据元素的地址。
总结。逻辑结构是面向问题的,物理结构是面向计算机的,其基本的目的就是将数据及其逻辑关系存储到计算机的内存中。
1.3数据类型
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
1.3.1数据类型定义
数据类型是按照值的不同划分的。数据类型可以分为两类:
(1)原子类型:是不可以再分解色基本类型,包括整型、实例、字符型等。
(2)结构类型:由若干个类型组合而成,是可以再分解的。例如,整型数组是由若干个整型数组组成的。
1.3.2抽象数据类型
抽象是指抽取出食物具有的普遍性的本质。
抽象数据类型;一个数学模型及定义在该模型上的一组操作
抽象数据类型体现了程序设计问题中问题分解、抽象和信息隐藏的特点。
抽象数据类型标准格式:
ADT 抽象数据类型名Data
数据元素之间逻辑关系的定义
Opeator
操作1
初始条件
操作结果描述
操作2
......
操作n
......
endADT
2.算法
2.1数据结构与算法的关系
数据结构是算法实现的基础,算法总是要依赖于某种数据结构实现,设计一种算法时会构建适合于这种算法的数据结构。数据结构是为了算法而出现的。
2.2算法定义
算法是求解特定问题的步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
2.3算法的特性
算法具有五个基本特性:输入、输出、又穷性、确定性、可执行性。
算法具有零个或多个输入,至少有一个或多个输出。
有穷性:指算法在执行有限步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
确定性:算法的每一步骤都具有明确的含义,不会出现二义性。
可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
2.4算法设计的要求
正确性、可读性、健壮性、高效率和低存储需求
2.5算法效率的度量方法
事后统计法(不科学、不准确),事前分析估算发
.2.6算法复杂度
算法的复杂性体现在运行该算法时占用计算机资源的多少上,计算机最重要的资源是时间(cpu)和空间(内存),所以复杂度分为时间复杂度和空间复杂度。
时间复杂度指的是运行算法所需要的计算工作量。
空间复杂度指的是执行这个算法所需要的内存空间。
2.6.1时间复杂度
时间复杂度最简单描述是:计算机运行一个算法时,程序被代码执行的总次数。
用打O记法来体现时间复杂度。
推导大O阶方法:
(1)用常数1取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在切且其系数不是1,则去除与这个项目相乘的系数
得到的结果就是大O阶。
常用时间复杂度所耗费的时间从小到大依次是:
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
2.6.2空间复杂度
空间复杂度指的是算法消耗的内存空间,也是问题规模(需要处理的数据量)n函数。
3线性表
3.1线性表的定义
线性表定义:零个或多个数据元素的有限序列。
如下图:
我们以上图a3为目标点,则a2为a3的直接前驱元素,s4为a3的直接后继元素。
所有线性元素的个数n定义为线性表长度,当n=0时,称为空表。
在表中每个数据元素都有自己的位置,比如a1是第一个元素。a6是最后一个元素,那个1和6就是线性表中的位序(常用i表示)。
在复杂的线性表中,一个数据元素可以由多个数据项组成。
3.2线性表的抽象数据类型
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,......,an},每个元素的类型均为DataType。其中,除了第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个后继元素。数据元素之间是一一对应的关系。
Operation
InitList(*L):初始化操作,建立一个空的线性表。
ListEmpty(L):如果线性表为空,返回true,否者返回false。
ClearList(*L):将线性表清空。
GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。
LocateElem(L,e):在线性表L中查找与给定值e相等的元素 ,如果查找成功,返回该元素在表中的序号,否则,返回零表示失败。
ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e;
ListDelete(*L,i,e):删除线性表L中第i个位置的元素,并用e返回其值。
ListLength(L):返回线性表L中的元素个数。
上述操作是最基本操作,如果实际问题中碰到关于线性表的复杂操作,可以用这些操作的组合来实现。如union(合集)操作:
/*将所有的在线性表Lb中但不在La中的数据元素插入到La中*/
void unionL(Sqlist *La,SqlistL Lb)
{
int La_len,Lb_len,i;
ElemType e; //声明La和Lb相同的数据元素e
La_len=ListLength(*La); //求线性表的长度
Lb_len=ListLength(Lb);
for (i = 1; i < Lb_len; ++i) {
GetElem(Lb,i,&e); //取Lb中第i个数据元素赋给e
if(!LoccateElem(*La,e)){ //La中不存在和e相同的数据元素
ListInsert(La,++La_len,e); //插入
}
}
}
重点原则:
当你传递一个参数给函数的时候,这个参数会不会在函数内部被改动决定了使用什么参数形式。
如果需要被改动,则需要传递指向这个参数的指针。
如果不需要改动,可以直接传递这个参数
3.3线性表顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
C语言的话,可以用一维数组来实现顺序存储结构。
来看顺序存储的结构代码。
#define MAXSIZE 20 //存储空间初始化分配量
typedef int ElemType; //ElemTypel类型根据实际情况而定,这里为int
typedef struct
{
ElemType data[MAXSIZE]; //数组,存储数据元素
int length; //线性表当前长度
}Sqlist;
描述顺序存储结构需要三个属性:
(1)存储空间的起始位置:数组data,他的存储位置就是存储空间的位置。
(2)线性表的最大容量:数组长度MAXSIZAE。
(3)线性表当前长度:length。
注意线性表长度应小于等于数组长度。
3.4顺序存储结构的插入与删除
3.4.1获得元素的操作
对于线性表的顺序存储结构来说,如果要实现GetElem操作,即将线性表L中的第I个位置元素值返回。很简单,直接看代码吧。
#define OK 1
#define ERROR 0
/*Status是函数的类型,其值是函数结果状态代码,如ok等*/
typedef int Status;
//初始条件:线性顺序表L已存在,1<=i<<ListLength(L)
//操作结果:用e返回L中的第i个数据元素的值,注意i是指位置,第i个位置的数组是从0开始
Status GetElem(Sqlist L,int i,ElemType *e)
{
if(L.length==0||i<1||i>L.length){return ERROR;}
*e=L.data[i-1];
return OK;
}
3.4.2插入操作
插入算法思路:
(1)如果插入位置不合理,抛出异常。
(2)如果线性表长度大于等于数组长度,则抛出异常或动态增加容量。
(3)从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置。
(4)将要插入元素填入位置i处。
(5)表长度加1。
实现代码如下:
//初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)
//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status ListInsert(Sqlist *L,int i,ElemType e)
{
int K;
if(L->length==MAXSIZE){return ERROR;} //线性顺序表已经满
if(i<1||i>L->length+1){return ERROR;} //当i比第一位置小或者比最后一位置还要大时
if(i<=L->length) //插入元素不在表位
{
for(K=L->length-1;K>=i-1;K--) //将要插入位置后的元素向后移一位
{
L->data[K+1]=L->data[K];
}
}
L->data[i-1]=e; //将新元素插入
L->length++;
return OK;
}
3.4.3删除操作
删除算法思路:
(1)如果删除位置不合理,抛出异常;
(2)取出删除元素;
(3)从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置;
(4)表长度减1。
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:删除L的第I个数据元素,并用e返回其值,L长度减1
Status ListDelete(Sqlist* L,int i, ElemType* e)
{
int k;
if(L->length==0){return ERROR;} // 线性表为空
if(i<1||i>L->length){return ERROR;} //删除位置不正确
*e=L->data[i-1];
if(L->length) //如果删除的不是最后位置
{
for (k = i; k<L->length;k++) { //将删除位置后继元素前移
L->data[k-1]=L->data[k];
}
}
L->length--;
return OK;
}
3.4.3线性顺序存储结构的特点
优点:无须为表示表中元素之间的逻辑关系而增加额外的存储空间;可以快速的存取表中任一位置的元素。
缺点:插入和删除操作需要移动大量元素;当线性表变化较大时,难以确定存储空间容量;造成存储空间的碎片。
3.4.5线性存储结构完整代码
//sqList.h
#ifndef DATA_STRUCTURE_SQLIST_H
#define DATA_STRUCTURE_SQLIST_H
#define OK 1
#define ERROR 0
#define MAXSIZE 20 //存储空间初始化分配量
typedef int ElemType; //ElemTypel类型根据实际情况而定,这里为int
typedef int Status; /*Status是函数的类型,其值是函数结果状态代码,如ok等*/
typedef struct
{
ElemType data[MAXSIZE]; //数组,存储数据元素
int length; //线性表当前长度
}Sqlist;
//初始化操作,建立一个空的线性表。
void InitList(Sqlist* L);
//如果线性表为空,返回true,否者返回false。
Status ListEmpty(Sqlist L);
//将线性表清空。
void ClearList(Sqlist* L);
//将线性表L中的第i个位置元素值返回给e。
Status GetElem(Sqlist L,int i,ElemType *e);
//在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号,否则,返回零表示失败.
Status LocateElem(Sqlist L,const ElemType* e);
//在线性表L中第i个位置插入新元素e;
Status LisInsert(Sqlist* L,int i,ElemType e);
//删除线性表L中第i个位置的元素,并用e返回其值。
Status ListDelete(Sqlist* L,int i,ElemType* e);
//返回线性表L中的元素个数。
Status ListLength(Sqlist L);
#endif //DATA_STRUCTURE_SQL IST_H
//sqList.cpp
#include "sqList.h"
//初始化操作,建立一个空的线性表。
void InitList(Sqlist* L)
{
for(int i=0;i<MAXSIZE;i++)
{
L->data[i]=0;
}
L->length=0; //将芫荽数据长度初始化
}
//如果线性表为空,返回true,否者返回false。
Status ListEmpty(Sqlist L)
{
if(L.length==0){
return OK;
}
return ERROR;
}
//将线性表清空。
void ClearList(Sqlist* L)
{
for(int i=0;i<MAXSIZE;i++)
{
L->data[i]=0;
}
L->length=0;
}
//初始条件:线性顺序表L已存在,1<=i<<ListLength(L)
//操作结果:用e返回L中的第i个数据元素的值,注意i是指位置,第i个位置的数组是从0开始
Status GetElem(Sqlist L,int i,ElemType *e)
{
if(L.length==0||i<1||i>L.length){return ERROR;}
*e=L.data[i-1];
return OK;
}
//在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号,否则,返回零表示失败.
Status LocateElem(Sqlist L,const ElemType* e)
{
for(int i=0;i<L.length;i++)
{
if(L.data[i]==*e)
{
return i;
}
}
return 0;
}
//初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)
//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status LisInsert(Sqlist* L,int i,ElemType e)
{
int K;
if(L->length==MAXSIZE){return ERROR;} //线性顺序表已经满
if(i<1||i>L->length+1){return ERROR;} //当i比第一位置小或者比最后一位置还要大时
if(i<=L->length) //插入元素不在表尾
{
for(K=L->length-1;K>=i-1;K-- ) //将要插入位置后的元素向后移一位
{
L->data[K+1]=L->data[K];
}
}
L->data[i-1]=e; //将新元素插入
L->length++;
return OK;
}
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:删除L的第I个数据元素,并用e返回其值,L长度减1
Status ListDelete(Sqlist* L,int i, ElemType* e)
{
int k;
if(L->length==0){return ERROR;} // 线性表为空
if(i<1||i>L->length){return ERROR;} //删除位置不正确
*e=L->data[i-1];
if(L->length) //如果删除的不是最后位置
{
for (k = i; k<L->length;k++) { //将删除位置后继元素前移
L->data[k-1]=L->data[k];
}
}
L->length--;
return OK;
}
//初始条件:顺序线性表L已存在
//操作结果:返回线性表L长度
Status ListLength(Sqlist L)
{
return L.length;
}
#include"sqList.h"
#include <iostream>
using namespace std;
int main(){
ElemType e=0;
Sqlist L;
InitList(&L);
for(int i=1;i<MAXSIZE;i++)
{
LisInsert(&L,i,i+10);
}
for(int i=0;i<MAXSIZE;i++){
cout<<L.data[i]<<' ';
}
cout<<endl;
cout<<ListEmpty(L)<<endl;
cout<<ListLength(L)<<endl;
cout<<LocateElem(L,&e)<<endl;
cout<<GetElem(L,5,&e)<<endl;
for(int i=1;i<5;i++)
{
ListDelete(&L,i,&e);
}
for(int i=1;i<MAXSIZE;i++){
cout<<L.data[i]<<' ';
}
cout<<endl;
cout<<ListEmpty(L)<<endl;
cout<<ListLength(L)<<endl;
cout<<LocateElem(L,&e)<<endl;
cout<<GetElem(L,5,&e)<<endl;
ClearList(&L);
}
3.4.6动态顺序表,使用顺序存储,数据容量动态增加
//sqList.h
#ifndef SQLIST_H
#define SQLIST_H
/*
* 动态顺序表,使用顺序存储,数据容量动态增加
* 外部按照[1...n]的方式进行访问
* 内部使用C语言实现,所以内部的索引从0开始
* */
typedef int Element;
typedef struct {
Element *e lem; // 顺序表元素空间的首地址
int len; // 记录顺序表的元素个数
int capacity; // 记录顺序表的容量
}SqList;
SqList *createSqList(int n); // 产生容量为n个元素的顺序表
void releaseSqList(SqList *seq); // 释放这个顺序表
// 获取顺序表中某个位置的值
int getSqList(SqList *seq, int pos, Element *ele);
// 查找顺序表中元素的位置
int locateSqList(SqList *seq, Element ele);
// 向顺序表指定位置上插入元素
int insertSqList(SqList *seq, int pos, Element ele);
// 显示顺序表里的元素
void showSqList(SqList *seq);
// 从顺序表中指定位置处删除元素
int deleteSqList(SqList *seq, int pos);
#endif
//sqList.c
#include <stdlib.h>
#include <stdio.h>
#include "sqList.h"
SqList *createSqList(int n) {
SqList *seq = (SqList *)malloc(sizeof(SqList));
if (seq == NULL) {
printf("malloc error!\n");
return NULL;
}
// 初始化顺序表里的每个元素
seq->elem = (Element *)malloc(sizeof(Element) * n);
for (int i = 0; i < n; ++i) {
seq->elem[i] = 0;
}
seq->len = 0;
seq->capacity = n;
return seq;
}
void releaseSqList(SqList *seq) {
if (seq) {
if (seq->elem) {
free(seq->elem);
}
free(seq);
}
}
/* [1 2 ... seq->len] pos的用户访问范围
* */
int getSqList(SqList *seq, int pos, Element *ele) {
if (pos < 1 || pos > seq->len) {
printf("pos invalid!\n");
return -1;
}
*ele = seq->elem[pos - 1];
return 0;
}
int locateSqList(SqList *seq, Element ele) {
for (int i = 0; i < seq->len; ++i) {
if (seq->elem[i] == ele) {
return i + 1;
}
}
return 0;
}
/* 插入操作:
* 外部范围:[1 2 ... seq->len],插入行为,范围最后 seq->len+1
* */
int insertSqList(SqList *seq, int pos, Element ele) {
if (pos < 1 || pos > seq->len + 1) {
printf("pos invalid!\n");
return -1;
}
// 是否容量满足
if (seq->len + 1 > seq->capacity) { // 容量越界,就扩容
printf("enlarger!\n");
Element *tmp = (Element *) malloc(sizeof(Element) * seq->capacity * 2);
if (tmp == NULL) {
printf("malloc error!\n");
return -1;
}
// 把老空间的值拷贝给新空间
for (int i = 0; i < seq->len; ++i) {
tmp[i] = seq->elem[i];
}
seq->capacity = seq->capacity * 2;
free(seq->elem);
seq->elem = tmp;
}
// 将pos位置后面的元素往后移,从seq->len逐步靠近pos的位置
for (int i = seq->len - 1; i >= pos - 1; --i) {
seq->elem[i + 1] = seq->elem[i];
}
seq->elem[pos - 1] = ele;
seq->len++;
return 0;
}
void showSqList(SqList *seq) {
for (int i = 0; i < seq->len; ++i) {
printf("%d\t", seq->elem[i]);
}
printf("\n");
}
int deleteSqList(SqList *seq, int pos) {
if (pos < 1 || pos > seq->len) {
printf("pos invalid!\n");
return -1;
}
// 从pos+1开始到最后的位置处,往前赋值
for (int i = pos; i < seq->len; ++i) {
seq->elem[i - 1] = seq->elem[i];
}
seq->len--;
return 0;
}
#include <stdio.h>
#include "sqList.h"
int main() {
int n = 5;
SqList *sqList = createSqList(n);
// 操作这个顺序表
for (int i = 0; i < n; ++i) {
insertSqList(sqList, i + 1, i+100);
}
showSqList(sqList);
insertSqList(sqList, 3, 300);
showSqList(sqList);
insertSqList(sqList, 9, 900);
showSqList(sqList);
printf("===========================\n");
deleteSqList(sqList, 4);
showSqList(sqList);
releaseSqList(sqList);
return 0;
}
3.5线性表顺序链式存储结构
3.5.1顺序存储结构不足的解决办法
线性表的顺序存储结构是有缺点的,最大的缺点就是插入和删除时需要移动大量的数据,为了解决这个问题所以采用链式存储结构。
3.5.2线性表链式存储结构定义
线性表的链式存储结构的特点是用一组任一的存储单元存储线性表的数据元素,这些存储单元可以使连续的也可以是非连续的。
在链式存储中为了表示每个数据元素与其后继数据元素之间的逻辑关系,除了要存储数据元素信息外,还要存储他后继元素的存储地址,我们把存储数据元素的域称为数据域,把存储后继元素位置的域称为指针域,指针域中存储的信息的域称为指针或链。这两部分信息组成数据元素的存储映像,称为j结点(node)。
n个结点链接成一个链表,即为线性表的链式存储结构,因此链表的每个节点中只包含一个指针域,所以叫做单链表。
对于线性表,我们把链表中第一个结点的存储位置叫做头指针,线性表最后一个节点指针为空。
为了方便对链表进行操作,会在单链表的第一个结点前附设一个节点,称为头结点。也可以存放线性表长度等附加信息,头结点的指针域指向第一个结点的指针。
3.5.3头指针与头结点的异同
3.5.4线性表链式存储结构代码描述
若线性表为空表,则头结点的指针域为“空”。
链式存储结构图解
单链表中,我们在C语言中可用 结构指针来描述。
#define OK 1
#define ERROR 0
typedef int ElemType;
typedef int Status;
typedef struct Node *LinkList;//定义LinkList
//线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
从上述结构定义中可知,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
3.5.5单链表的读取
算法思路:
1.声明一个节点p指向链表第一个结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针往后移动,不断指向下一结点,j累加1;
3.若链表末尾p为空,则说明第i个元素不存在;
4.否则查找成功,返回结点p的数据。
实现代码算法如下:
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L,int i,ElemType* e){
int j=1; //j为计数器
LinkList p; //声明一结点p
p=L->next; //让p指向链表L的第一个结点
while(p&&j<i) //p不为空或者计数器j还没有等于i时,循环继续
{
p=p->next; //让p指向下一个结点
++j;
}
if(!p||j>i){
return ERROR; //第i个元素不存在
}
*e=p->data; //取第i个元素的数据
return OK;
}
主要核心思想就是“工作指针后移”。
3.5.6单链表的插入与删除
先看单链表的插入。假设存储元素e的结点为s,要实现的结点p、p->next和s之间逻辑关系的变化,只需要将结点s插入到结点p和p->next之间即可。
根本不需要惊动其他节点,只需要让s->next和p_next指针做一下变化就是了
s->next=p->next; //将p的后继节点赋值给s的后继
p->next=s; //将s赋值给p的后继
就两句代码,就是让p的后继结点改成s的后继结点,再把结点s变成p的后继节点。
注意以上两句是不能反着写的。插入节点s后,链表如下图
对于单链表的标头和表位的特殊情况,操作是相同的。
单链表第i个数据插入节点的算法思路:
(1)声明一指针p指向链表头结点,初始化j从1开始;
(2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
(3)若到链表末尾p为空,则说明第i个元素不存在;
(4)否则就查找成功,在系统中生成一个空结点s;
(5)将数据元素e赋值给s->>data;
(6)单链表插入标准语句s->next=p->next;p->next=s;
(7)返回成功
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p=*L;
j=1;
while(p&&j<i) //寻找第i个结点
{
p=p->next;
++j;
}
if(!p||j>i){
return ERROR; //第i个元素不存在
}
s=new Node; //生成新结点
s->data=e;
s->next=p->next;
p->next=s;
return OK
}
现在我们再来看单链表的删除
(到这为止感觉看书上的代码,没有课上的代码好看,后面的部分全是01星球数据结构直播课的笔记了)