数据结构(才学一点点)《大话数据结构》 学习笔记 2002/5/15

研读《大话数据结构》书中有许多错误,但不妨为小白入门数据结构的书,有许多博主专门为其勘错,结合别人的纠错看,万万不可建立错误的理念,全书可以不看完,之后去研读数据结构大作。(懒得传图,附带连接有图版)

本文有图纯享版:

链接:https://pan.baidu.com/s/1DhKA159oIeb28QNRvOZ7nw?pwd=Date
提取码:Date

《大话数据结构》2011版:

链接:https://pan.baidu.com/s/1G6hPJyw0kwgVMpeJcm8p_w?pwd=Date
提取码:Date

数据结构

序言

![屏幕截图 2022-05-12 144302](image/DateStract/屏幕截图 2022-05-12 144302.png)

数据结构是互相之间存在的一种或者多种特定关系的数据元素集合。同样是结构,从不同角度讨论会有不一样的分类:

![](image/DateStract/屏幕截图 2022-05-12 144704.png)

读后感:

​ 顺序存储结构与连接存储结构在操作上十分相似,不同的是在于内存扩展的容易程度,在操作上,顺序存储结常利用下标,而链接存储结构利用下一结点的头结点地址。

​ 固定存储空间大小有好处有坏处,在多个空间大小相似时,可以节省时间开销。动态空间大小在多个空间大小相差较大时节省存储空间,但是有更多系统开销,时间开销更多。

屏幕截图 2022-05-12 144829屏幕截图 2022-05-12 144815屏幕截图 2022-05-12 144746屏幕截图 2022-05-12 144804

image-20220512145045644

image-20220512145110528

image-20220513165106129

算法的时间复杂度 O( )

大写O()是算法时间复杂度记法(大O记法)。

推导大O阶:

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中只保留最高阶项。
  3. 如果最高项存在且不是1,则去除与这个项相乘的常数,得到的结果就是大O阶。

其实理解大0推导不算难,难的是对数列的一些相关运算,这更多的是考察你的数学知识和能力


image-20220512153342253

常数阶O(1):

int sum = O,n- 100;
sum = (1+n) *n/2;
printf ("&d", sum);

​ 这个算法的运行次数函数式f = (3)。现将3化为1。保留最高项时发现没有最高项,所以时间复杂度为O(1)

int sum = 0, n= 100;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
printf ("&d", sum);

他的时间复杂度为O(1)


线性阶O(n)

for(int i = 0; i < n ; i++){
    printf("A");//时间复杂度为o(1)步骤
}

循环体内代码执行n此,得到时间复杂度为O(n)。

对数阶O(log n)

int a = 1;
while(a < n){
	a = a * 2;//时间复杂度为o(1)步骤
}

循环次数x : a*2x == n; 所以 x = log n。 所以他的时间复杂度为O(log n )。

平方阶O(n2)

int i,i;
for (i= 0; i < n; i++){
	for (j = o; j < n; j++){
		/*时间复杂度为O(1)的程序步骤序列*/
	}
}

循环次数: n * n 。 故时间复杂度为O(n2)

int i,j;
for (i = 0; i < n; i++){
    for (j=i; j < n: j++) {
        /*时间复杂度为O(1)的程序步骤序列*/
    }
}

次数: n +(n - 1) + (n - 2)+... + 1 = n*(n+1)/2 = n2/2 + n/2

故时间复杂度为O(n2)


平均运行时间是所有情况中最有意义的,因为他是期望的运行时间,但一般都是通过运行一定数量得到的。

在没有特殊说明的情况下,都指的是最坏时间复杂度。


空间复杂度S(n)

在一些计算算法中,可以牺牲一些空间开销换取计算时间。

一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为0(1)。

略........


线性表

零个或者多个具有相同类型的数据元素的有限序列,链表,栈,队列......都是其的子类。

image-20220513093350512

线性表的顺序存储结构(地址连续)

image-20220512161729542

image-20220512161644993

image-20220512173147503


线性表的链接存储结构(地址不连续)

image-20220512172641504

image-20220512173400564

image-20220512173412137

image-20220512173423571

image-20220512174841077


静态链表

image-20220512182959243

image-20220512183017615

若之后遍历数组,不根据下标,而是根据cur存储的数字进行遍历,下标确定数据对象的位置,cur决定数据对象的顺序,最后一位下标保存开头的cur,若想访问开头的car,必须访问 数组长度-1 下标位置的car。(本质上是有些语言没有指针,需要自己定义,可以凭借数组完成这样的操作,建立自己的静态链表,不仅有下标访问,同时数据对象还存储类似于指针的cur(int) 保存下一个你想让他指向的数据对象的下标)是一种非常巧妙的办法。

静态链表原理实现:

image-20220512183934028


image-20220512183727220

末端插入实现:

image-20220512184029345

StaticList[0].cur = 7

是因为之后有末端插入工作,将第一个空闲空间下标存储在这,插入时直接根据此找到位置,无需遍历数组。


中间插入实现,申请结点的malloc ()

由于cur存储下一个数据对象的下标位置(相当于动态链表的指针),故可以不在存储空间顺序排列,直接加在存储空间末尾,修改插入位置前后的car,即可实现不在空间上挪动数据,逻辑上插入。(与前面动态链表原理相似,只不过提前开辟了内存空间)

image-20220512184721322

实现:

image-20220512184657572


删除数据对象实现(释放结点的 free() )

​ 理解了前面插入数据对象后,删除对象十分简单

Status ListDelect(StaticLinkList L, int i)
{
    int j,k;
	if(i < 1 || i > ListLength(L))
        return ERROR
	k = MAX_SIZE - 1;
    for(j = 1; j <= i-1, j++)
        k = L[k].cur;
    j = L[k].cur;
    L[k].cur = L[j].cur;
    Free_SSL(L,j);
    return OK;
}

暂且跳过,没看懂

image-20220512205631948


循环链表

​ 将单链表中终端结点的指针端由空指针改为指向头指针,使整个单链表形成一个环,这种头尾相接的单链表成为单循环链表,简称循环链表(circular linked list)

循环链表解决了

无法找到他的前驱结点的问题,同时将头结点作为终止条件即可遍历数组,可以从任意一结点出发,访问链表全部结点。

image-20220513074154249

同时还能连接两循环链表:

image-20220513074233348


双向链表

image-20220513092423736

用空间换取时间


栈与队列

image-20220513094505306

image-20220513094555114

栈是一种特殊的线性表


栈的顺序存储结构

image-20220513095152119

image-20220513095217698


Push操作:

image-20220513095325715

image-20220513095337571


pop操作:

image-20220513095416009


相同数据类型的栈,一个满一个空时,两者可以共享内存。

image-20220513100453263


栈的链式存储结构

image-20220513101126921

image-20220513101144952


push:

image-20220513101258886

image-20220513101318772

image-20220513101326707


pop:

image-20220513101429012

image-20220513101440414

image-20220513101535740


栈的作用

​ 栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。所以现在的许多高级语言,比如Java、C#等都有对栈结构的封装,你可以不用关注它的实现细节,就可以直接使用Stack的push和pop方法,非常方便。

​ 结合下面的应用,个人理解栈:

栈是一种特殊的线性表,相比于前面的链表,他省略了下标处理等一系列操作,更加关注与问题的解决,例如四则运算,只专注于如何处理表达式。类似于舍弃了链表原先的方法,写入pop,push操作来处理数据。


栈的应用——递归

image-20220513102653986

​ 选代和递归的区别是:选代使用的是循环结构,递归使用的是选择结构。递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。选代则不需要反复调用函数和占用额外的内存。因此我们应该视不同情况选择不同的代码实现方式。

​ 对于高级语言,递归问题的细节实现不需要用户管理栈。但递归时就是根据一个外在条件不断判断是否结束,期间栈中不断push入一段计算结果的代码(栈处于不断push入的状态),最后达到终止条件时,计算结果的代码全部pop,对应的结果不断更新至得到答案。


——四则运算

后缀(逆波兰)表达法定义
中缀表达式转后缀表达式

中缀表达式: 9 + (3 - 1) * 3 + 10 / 2

后缀表达式: 931-3*+102/+

两步:

  1. 将中缀表达式转化为后缀表达式(栈用来进出运算符号)
  2. 将后缀表达式进行运算得出结果(栈用来进出运算数字)

第一步:

9 + (3 - 1) * 3 + 10 / 2

从左到右遍历整个表达式,若是数字直接输出,push符号,当即将push的符号优先级低于top的符号优先级则输出栈内所有符号,若表达式输出完毕,输出所有栈内符号。遇到括号(时,先正常处理,直到遇到)闭合后,将()内全部符号输出,:

image-20220513111855249

输出9 , push + , 下一个遇到了( 由于未闭合,继续push - 直到遇到)闭合,将()内符号输出。931-

后 push * 因为 + 优先级低于 * 不输出,继续输出 3

image-20220513112413911

由于下一个符号 + 的优先级小于 * , pop * , pop +

(先检查符号,后进行push, pop)


队列queue

image-20220513115336079

image-20220513115425691

队列在程序设计中用得非常频繁。比如用键盘进行各种字母或数字的输入,到显示器上如记事本软件上的输出,其实就是队列的典型应用

queue的抽象数据类型

image-20220513115602693

image-20220513155641880


串(字符串)

image-20220513155811094

image-20220513155853466

image-20220513155956603

​ 计算机中的常用字符是使用标准的ASCII编码,更准确一点,由7位二进制数表示一个字符,总共可以表示128个字符。后来发现一些特殊符号的出现,128个不够用,于是扩展ASCII码由8位二进制数表示一个字符,总共可以表示256个字符,这已经足够满足以英语为主的语言和特殊符号进行输入、存储、输出等操作的字符需要了。可是,单我们国家就有除汉族外的满、回、藏、蒙古、维吾尔等多个少数民族文字,换作全世界估计要有成百上千种语言与文字,显然这256个字符是不够的,因此后来就有了Unicode编码,比较常用的是由16位的二进制数表示一个字符,这样总共就可以表示216个字符,约是65万多个字符,足够表示世界上所有语言的所有字符了。当然,为了和ASCII码兼容, Unicode的前256个字符与ASCII码完全相同

串的顺序存储结构

​ 串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。既然是定长数组,就存在一个预定义的最大串长度,一般可以将实际的串长度值保存在数组的0下标位置,有的书中也会定义存储在数组的最后一个下标位置。但也有些编程语言不想这么干,觉得存个数字占个空间麻烦。它规定在串值后面加一个不计入串长度的结束标记字符,比如“\0”来表示串值的终结,这个时候,你要想知道 串的长度,只需要遍历计算一下。

​ 对于串的顺序存储,有一些变化,串值的存储空间可在程序执行过程中动态分配而得。比如在计算机中存在一个自由素者区,叫做“堆”。这个堆可由C 语言的动态分配函数malloc ()和free ()n

串的链式存储结构

image-20220513162315482

串的朴素模式匹配算法

image-20220513162620753

image-20220513162634551

image-20220513163140405

image-20220513163206816

image-20220513163216825


KMP模式匹配算法

image-20220513163844074

通过这种匹配,减少匹配次数

image-20220513164556231

略。。。。。。之后再看


image-20220513165601012

子树的个数没有限制,但他们一定是互不相交的

image-20220513165818154

image-20220513170027940

image-20220513170154124

image-20220513170203502

image-20220513170239035

image-20220513170331454

image-20220513170346795

树的抽象数据类型

image-20220513170420579

树的存储结构

对于树的存储结构,如果直接用顺序存储结构,无法反应各个结点的逻辑关系,但是我们可以应用顺序存储结构与链式存储结构的特点来实现存储

双亲表示法(结点存入双亲地址/数组下标,顺序结构)

image-20220513170912881

image-20220513171042164

image-20220513171143221

加入长子域:(第一个孩子的下标,若没有则设为-1)

image-20220513171536127

加入兄弟域:(有了长子域后再加入次子域,若没有设为-1)

image-20220513171811324

孩子表示法(链表)

image-20220513172652179

image-20220513172816828

image-20220513172840989

双亲孩子表达法

也可以结合两个方法的优缺点

image-20220513173513827

孩子兄弟表达法

image-20220513173739154

image-20220513173746465

这个方法相比于前面,更能快速找到某个结点的孩子。前面的方法如果要进入下一结点,需要经过兄弟,但是孩子兄弟表达法可以从一个结点的下标二,快速到该节点的孩子,但这不是最重要的好处。

image-20220513173859563

前面的方法是以树的层为划分,而二叉树是以结点为研究对象


二叉树

引子:

现在我们来做个游戏,我在纸上已经写好了一个100以内的正整数数字,请大家想办法猜出我写的是哪一个?注意你们猜的数字不能超过7个,我的回答只会告诉你是“大了”或“小了”。

image-20220513174714251

特殊的树状结构,二叉树

image-20220513174800518

二叉树的特点

image-20220513175112597

Top To Bottom

image-20220513121701458

posted @ 2022-05-15 11:22  奇迹和魔法都是存在的  阅读(74)  评论(0编辑  收藏  举报