408—DS(笔记自用)

(纯手打,有误或缺知识点待改,有误请以书本为准!!!)

二级标题:这一节所有的思维导图

三级标题:标题+概念+图+注意事项🐖1 (1)①  a ▲

第一章 绪论

1.1 数据结构的基本概念

1 基本概念

(1)数据

        信息载体、能输入到计算机并被计算机程序识别和处理的符号的集合(二进制0和1)

(2)数据元素和数据项

        数据元素是数据的基本单位,组合项是由多个数据项组成的       

        

(3)数据结构和数据对象

        数据结构:某个门店之间的数据元素之间的关系

        数据对象:所有门店数据元素的集合,即集合中的数据元素有相同的性质

        

(4)数据类型和抽象数据类型

        数据类型:值的集合和一组操作。原子类型:int, 结构类型:结构体

        抽象数据结构ADT:抽象数据组织及相关操作,用数学化的语言定义数据的逻辑结构和运算,与具体实现无关,不关心存储结构。

        

2 数据结构的三要素

(1)逻辑结构

        数据元素之间的逻辑关系

        集合,线性结构(前驱后继),树形结构(一对多),图结构(多对多)

        

(2)物理结构/存储结构

        用计算机表示数据元素的逻辑关系

        e.g.线性结构的存储结构

        顺序存储:逻辑上相邻,物理位置上也相邻

        链式存储:逻辑上相邻,物理位置可以不相邻,借助指针

        索引存储:用索引表来找数据元素在内存中的物理位置

        散列存储:用关键字算出物理位置

              

        🐖:存储结构会影响空间分配的方便程度和数据运算的速度

                抽象数据结构定义一个数据结构,确定了存储结构才能实现数据结构,存储结构不同会导致运算的具体实现不同

(3)数据的运算

        施加在数据上的运算的定义和实现,运算的定义是针对逻辑结构的,指出运算的功能(入队出队...),运算的实现是针对存储结构的,指出运算的具体操作步骤(具体是挨着存还是靠指针...)

1.2  算法和算法评价

1 基本概念

(1)算法:求解问题的步骤

(2)算法的特性

        有穷性:执行有穷步,每一步有穷时间内完成

        确定性:相同的输入只能得到相同的输出

        可行性:基本运算执行有限次实现

        输入:零个或多个

        输出:一个或多个

        🐖:算法必须有穷,程序可以无穷,微信、死循环不是算法

(3)好算法特质

        正确性

        可读性

        健壮性:输入非法数据时,适当做出反应或进行处理

        高效率与低存储量需求:时间复杂度和空间复杂度低

2 时/空复杂度

(1)时间复杂度

        ①事先预估算法时间开销T(n)和问题规模n的关系。不能采用事后统计运行时间的方法评估算法的时间开销,存在和算法本身无关的外界因素和比较严重不能事后统计的算法

        ②多项相加保留最高项并将系数变为1,多项相乘都保留。 更高项的判别:常对幂指阶

        ③顺序执行的代码只会影响常数项可以忽略,只需挑循环中的一个基本操作分析执行次数和n的关系,如果有多层嵌套循环只需关注最深层循环循环了几次

        a. 单层循环的栗子

                ▲循环里的操作每次翻倍,O(log_{2}n)

                设次数x解和n的关系

                

              ▲时间复杂度和输入顺序有关,需要考虑不同情况算平均情况

                

        b. 嵌套循环的栗子

                ▲外n内n,O(n^{2})

                

(2)空间复杂度

        ①空间开销和问题规模之n间的关系

        ②只需关注存储空间大小与问题规模相关的变量

        a. 无论问题规模怎么变,算法运行所需的内存空间都是固定的常量(原地工作),S(n)=O(1)

        b. 定义了和问题规模有关的变量(数组),S(n)=O(n) / S(n)=O(n^{2}) / S(n)=O(n^{2})

 ​​

        ③函数递归调用带来的内存增加

        a. 函数中没有定义和问题规模有关的变量,空间复杂度=递归调用的深度,S(n)=O(n)

                 

        b. 递归时函数中还有和问题规模有关的变量(数组),要算出各级调用时的空间求和

                    

                S(n)=O(n^{2})

第二章 线性表

2.1 线性表的定义和基本操作

1 定义(逻辑结构)

        具有相同数据类型(占空间一样大)的n个数据元素的有限(不能是无穷个)序列(有次序)

2 基本操作

2.2 顺序表(存储方式1)

1 基本概念

(1)定义

        逻辑上相邻的元素物理位置上也相邻,用首地址+sizeof(元素类型)来找到具体某个数据位置

(2)顺序表的实现

        ①静态分配:ElemType data [MaxSize]

        ②动态分配:ElemType *data       

                             L.data = (ElemType*)malloc(sizeof(ElemType)*L.MaxSize)

                             L.data = (ElemType*)malloc(sizeof(ElemType)*(L.MaxSize+len))

                             大小可变但时间开销大,需要把数据复制到新空间

(3)顺序表的特征

        ①随机访问,查找的时间复杂度为O(1)

        ②存储密度高,不用花空间存指针

        ③拓展容量不方便,静态不可拓展,动态需要进行数据复制

        ④插入删除不方便,需要移动大量数据

2 插入、删除实现

(1)插入

        ①在第i个位置插入元素,需要将i及其以后的所有元素后移

        ②代码实现

        ​​​​​

        ③时间复杂度

(2)删除

        ①删除第i个位置元素,需要将从第i+1个元素开始全部前移

        ②代码实现

        

        ③时间复杂度

        

3 查找实现

(1)按位查找

        ①获取第i个位置的元素的值,注意下标和位置不同

        ②代码实现(静态分配和动态分配相同)

        

        ③时间复杂度:O(1),随机存取且每个元素大小相同

(2)按值查找

        ①在表中找给定元素,返回第一次出现的位序后结束

        ②代码实现(简单结构==、复杂结构要对比每一个元素)

        

        ③时间复杂度

        

2.3 链表(存储方式2)

1 单链表

(1)定义与实现

        ①定义:每个结点除了存放数据元素外,还要存储指向下一个结点的指针。不要求连续空间,改变容量方便,不可随机存取,耗费一定空间存放指针

🐖:* LNode == LinkList 当使用*Lnode更强调这是一个结点,使用LinkList强调这是一个单链表

        ②实现

        a. 不带头结点的单链表

        

        🐖:声明链表时是不创建结点的,初始化时传参要传引用才能实现NULL的赋值

        b. 带头结点的单链表

        

        🐖:L作为头节点不存储数据,要给L指向的第一个结点赋值为NULL,判空也是以第一个结点是否为null进行判断

(2)插入

        ①按位序插,给定插入位置

        a.带头结点

        

        🐖:p和j的都是当前扫描的结点,注意最后要p指向的是i-1即插入结点的上一个结点,平均时间复杂度O(n)

        b.不带头结点

        

        🐖:这里L是从1开始的,p和j的都是当前扫描的结点但注意初始赋值,需要单独写一段代码处理插入在初始位置的情况,平均时间复杂度O(n)

        ②按指定结点后插,已知结点位置

        

        🐖:时间复杂度O(1)

        ③指定结点前插

        a. 传入头指针,找到给出结点的上一个结点

        🐖:平均时间复杂度O(n)

        b. 在给出结点处后插一个结点并修改两个结点数值

        

        🐖:时间复杂度O(1)

(3)删除

        ①按位序删除,给定删除位置

        a. 带头结点

        

        🐖:p和j的都是当前扫描的结点,注意最后要p指向的是i-1即删除结点的上一个结点,需要引用类型把删除的元素带出,平均时间复杂度O(n)

        ②删除指定结点,只有指定结点信息,需要修改的是前驱节点的next

        a. 传入L指针找到前驱节点

        🐖:平均时间复杂度O(n)

        b. 把下一个结点的值复制到要删除的结点上,然后把下一个结点删了

        

        🐖:这种方法不适用删除最后一个结点,因为最后一个结点的下一个结点为空,没办法进行数值复制,时间复杂度O(1)

(4)查找

        ①按位查找

        

        🐖:头结点看成是第0个结点,i=0时返回头结点,小于或大于表长返回NULL,也可以让j从1开始然后如果i=0的话单独判断一下返回头指针L就行

        ②按值查找

        

        🐖:这里从第一个结点开始比较,时间复杂度O(n)

(5)求表长

🐖:时间复杂度O(n)

(6)单链表建立

        ①尾插

        🐖:让r指针一直指向最后一个结点,用s指针创建新结点,然后用r不断去追新结点,创建完成后,把r的next置空,时间复杂度O(n)

        ②头插

        

🐖:头插法必须对链表进行初始化的next=null(最好只要初始化链表都置空)然后对头结点进行后插,头插法重要应用链表逆置

2 双链表

(1)初始化

初始化前后指针均置空,DLinklist强调是个链表,*DNode强调是个结点

(2)插入

🐖:先连后面的,再连前面的,注意没有后继结点的情况

        如果需要按位序插,只需遍历到插入位置的上一个结点,再后插

        如果需要对结点进行前插,用prior找到上一个结点,对该结点后插

(3)删除

🐖:当p删除结点存在时,再判断p是不是最后一个,如果是最后一个不用改prior

        可以用依次删除头结点后继结点的方式销毁一个链表

        

(4)遍历

🐖:时间复杂度O(n)

3 循环链表

(1)循环单链表

        循环单链表就是把单链表表尾本来指向NULL的指针指向L头结点,注意判空和是否为最后一个结点的情况

    

🐖:循环单链表可以循环找到某个结点之前的结点

        普通单链表对于表尾的操作只能从头结点遍历,循环单链表可以让L指向表尾,这样L的next就是表头,对表头表尾操作都很方便这种情况下在表尾插入时需要改L指向

(2)循环双链表

        循环双链表就是把双链表表头和表尾本来指向NULL的指针分别指向L尾结点和L头结点(所有next形成闭环所有prior形成闭环)

  

🐖:循环双链表插入删除不用考虑是否为最后一个结点,只按顺序改指针就行

  

4 静态链表

(1)定义

        分配一整片连续空间,各个结点集中安置

        每个结点包括数组元素和下一个结点的数组下标,可以通过下标和每个元素所占空间大小定位具体元素

        

        

🐖:上面的更常见相当于a是一个Node型数组,下面的a本质上也是个Node型数组,不过不拆开看,而是看成一个整体静态链表

(2)基本操作

        ①初始化:a[0]的next设为-1,并用一个特殊数值标记其余空闲结点

        ②查找:从头结点挨个往后遍历(按逻辑位序不是物理位序),时间复杂度O(n)

        ③插入:找个空位置(初始化特殊标记过)放数据,从头遍历到i-1,改新结点的next和i-1结点的next

        ④删除:从头遍历找到前驱结点,修改前驱结点next为删除结点的next,删除结点置为特殊标记

5 顺序表链表对比

1、逻辑结构:线性表

2、存储结构

        ①顺序表

                顺序存储,随机存取,存储密度高

               大片连续空间分配不便,改变容量不便

        ②链表

                链式存储,不可随机存取,存储密度低

                离散的小空间分配方便,改变容量方便

3、基本操作:创、销、增、删、改、查

顺序表链表

预分配大片连续空间

分配方式:静态(容量不可改变),动态(容量可改变,但移动代价高)

只需一个头结点

Length=0

回收方式:静态(系统自动回收),动态(free)

依次free申请的结点
插入/删除元素将后续的元素后移/前移,时间复杂度O(n)来自元素移动修改指针,时间复杂度O(n)来自结点查找

按位查找,时间复杂度O(1)

按值查找,无序O(n)有序O(\log _{2}n)

按位查找,时间复杂度O(n)

按值查找,时间复杂度O(n)

        顺序表:表长可预估,查找操作多;链表:表长难预估,增加删除多

第三章 栈、队列和数组

3.1 栈

1 定义和基本操作

(1)定义

        只允许在一端进行插入删除操作的线性表

        栈顶、栈底、空栈

        后进先出LIFO

(2)基本操作

        创、销、增、删、查(通常是栈顶元素)

(3)常考题型

        给定进栈顺序判断合法的出栈顺序

        🐖:n个不同元素进栈,出栈元素不同排列的个数为\frac{1}{n+1}C_{2n}^{n}

2 顺序存储实现(存储方式1)

(1)定义

        top中放的是栈顶元素的下标,如果栈中只有一个元素top==0,内存中保存的是数据+top

        

(2)初始化、判空

        初始化top指针为-1表示栈中没有元素

           

(3)进栈

        先移指针再放元素,top指向的是当前栈顶元素的位置

        

(4)出栈

        先取元素再移指针,调用前在内存中申请了带回删除元素x的位置

        

(5)读栈顶元素

        

🐖:上述的(2)~(5)的操作都是在top指向当前元素的情况下实现的,如果初始化top=0,即让top指向下一个元素插入的位置,要注意对进/出元素和挪指针顺序的修改

因为顺序栈大小初始化固定,可以通过共享栈的方式提高顺序栈的利用率

3 链式存储实现(存储方式2)

(1)定义

        

(2)实现方法

        带头结点、不带头结点

(3)基本操作

        同单链表,不过只能在链头进行操作

3.2 队列

1 定义和基本操作

(1)定义

        只允许在一端进行插入,另一端进行删除的线性表

        队头、队尾、空队

        先进先出 FIFO

(2)基本操作

        

2 顺序存储实现(存储方式1)

(1)定义

        声明一个队列分配一片数组空间和两个指针

        

(2)初始化、判空

        如果规定front指向队头元素,rear指向队尾元素的后一个位置

        

(3)入队

        

        🐖:因为队列是从队头出队的,所以当rear==MaxSize时也不一定是满队列,注意这里rear指针不是简单的后移,而是取模将队列在逻辑上变成环形,可以利用队头释放了的空间即循环队列

        需要牺牲一个空间判断队列已满  

(4)出队

        出队front指针所指元素,并让指针转圈后移

        

(5)查队头元素

        

🐖:①判断队列已满/已空总结

        a.牺牲一个空间

        满:,空:

        满:,空:

        计算队列元素个数:

        b.在结构体中定义一个队列长度

        满:

        空:

        c.在结构体中定义一个标记做了删除/插入操作的标记

        只有删除才可能队空tag=0,只有插入才可能队满tag=1,再判断完front==rear后再判断做了什么操作

        满:

        空:

        ②上述所有情况是rear指向队尾元素的下一个,rear也可能会指向队尾元素

        a. 放元素和移指针的顺序

        b. rear初始化为n-1的位置(因为先移指针再放数据)

                

        c. 判空:队尾的下一个位置是队头 

        d. 判满:牺牲一个(front在rear后面两个为满),增加辅助变量

3 链式存储实现(存储方式2)

(1)定义

        为了方便入队和出队,定义两个指针,rear入队front出队

        

(2)初始化

        ①带头结点

        

        

        

        ②不带头结点

        

        

        

(3)入队

        ①带头结点

        从rear指向的队尾进行入队,结点插入后将rear指针指向新插入的队尾结点

        

        ②不带头结点

        对于插入第一个结点时需要单独考虑,将rear和front均指向新结点,后续插入在队尾做后插操作即可,每次后插完记得将rear指向新的队尾结点

        

(4)出队

        ①带头结点

        front指向头结点,出队头结点的后一个结点,当出队的是最后一个结点时,需要将队列恢复成空队状态

        

        ②不带头结点

        front指向第一个结点,直接出队front指向的结点,当出队的是最后一个结点时,需要将队列恢复成空队状态

        

4 双端队列

(1)定义

        允许从两端插入和删除的线性表

        

(2)判断输出序列合法性

        看清楚是输入受限还是输出受限

3.3 栈和队列的应用

1 括号匹配(栈)

        遇到左括号入栈,遇到右括号则出栈一个左括号,出栈后两者进行匹配

        失败:不匹配;扫描到右括号但栈空;全部扫描完后栈不空

2 表达式求值(栈)

核心就在于每次运算完的东西就看成新的一个数字,以及运算符在操作数的什么位置

(1)中缀表达式转后缀表达式

        ①手算:一个中缀表达式可以转换成不同的后缀表达式

        ②机算

        a. 只要左边运算符能先计算,就先算左边,这种转完符号是按运算顺序排的

                                

        b. 优先级:* /高于+ -  同级栈内高于栈外

             步骤:从左到右扫描,遇到操作数直接加入后缀表达式,遇到运算符如果比栈顶优先级高压入栈中,如果比栈顶优先级低或同级,依次弹出栈顶运算符(栈中有高优先级或同级),当有( 时直接入栈,从 ( 以后的运算符不用看( 以前压入栈中的运算符的优先级,直到遇到 )依次弹出栈中运算符直到 ( 为止,扫描完弹出栈中剩余所有运算符

              考点:某一步栈中元素,栈多大不会溢出

(2)后缀表达式计算

        ①手算:从左往右,遇到操作数不用管,遇到运算符,把运算符前两个操作符和该运算符合成一个新的操作数

        ②机算:从左往右,遇到操作数入栈,遇到运算符,从栈顶连着弹出两个元素和该运算符合成一个新的操作数压回栈顶,先弹出的是右操作数

(3)中缀表达式转前缀表达式

        ①手算:一个中缀表达式可以转换成不同的前缀表达式

        ②机算:只要右边运算符能先计算,就先算右边,这种转完符号是从右到左生效的

                

(4)前缀表达式计算

        从右往左扫描,先弹出的是左操作数,其余同后缀

(5)中缀表达式计算

        步骤:初始化操作数栈和运算符栈,遇到操作数入操作数栈,遇到运算符按照中缀转后缀逻辑入运算符栈,只要运算符栈中有运算符弹出,就从操作数栈栈顶取两个操作数进行合成,合成新操作数压回栈,直到运算符栈中空,操作数栈中只有一个数时结束

        🐖:中缀转后缀栈是用来存放当前不能确定运算次序的运算符后缀表达式计算栈是用来存放当前还不能确定运算次序的操作数

3 递归(栈)

(1)函数调用的特点:最后调用的函数最先执行结束

(2)函数调用栈:调用返回地址、实参、局部变量

        

(3)递归算法

        将原始问题转换成属性相同,规模较小的问题。需要有递归表达式和边界条件

(4)递归算法和栈的联系

        每进入一层递归,将递归调用信息(调用返回地址、实参、局部变量)压入栈顶。每退出一层递归,从栈顶弹出信息并返回到调用位置

        

        缺点:太多层可能导致栈溢出,包含重复运算

        如果想将递归转为非递归,可以自己定义一个栈

4 队列应用

(1)树的层次遍历:每次处理队头结点,如果有孩子结点,就插入队尾并出队,没有就直接出队

(2)图的广度优先遍历:每次处理队头结点,将相邻的没有被处理过的结点依次插入队尾并出队该结点,没有就直接出队

(3)在OS中的应用:多个进程使用有限的系统资源时先进先出策略;打印数据缓冲区

3.4 数组和特殊矩阵

1 一维数组存储结构

(1)数组元素大小相同,物理上连续存放

(2)可以通过起始地址计算任意元素存放地址,注意下标从0(LOC+i*sizeof(Elemtype))还是1开始(LOC+(i-1)*sizeof(Elemtype))

2 二维数组存储结构

(1)数组元素大小相同,物理上连续存放(行优先/列优先)

(2)可以通过起始地址计算任意元素存放地址

        ①行优先:注意下标从0(LOC+(i*N+j)*sizeof(Elemtype))还是1开始(LOC+((i-1)*N+(j-1))*sizeof(Elemtype))

        ②列优先:注意下标从0(LOC+(j*M+i)*sizeof(Elemtype))还是1开始(LOC+((j-1)*M+(i-1))*sizeof(Elemtype))

3 普通矩阵的存储

(1)二维矩阵,注意下标问题(一般矩阵是从1开始,数组是从0开始)

4 对称矩阵的压缩存储

  

(1)存上三角/下三角 + 对角线

(2)行优先/列优先

(3)数组大小:\frac{(1+n)*n}{2} ,对应数组最后一个元素下标为 \frac{(1+n)*n}{2}-1

(4)矩阵下标到数组下标的映射:算明白当前元素是第几个,数组从0开始就-1,从1开始下标就是第几个

        e.g.方便理解放个栗子

         

5 三角矩阵压缩存储

        

同对称矩阵,需要在数组末多一个空间放常量

6 三对角矩阵的压缩存储

        

(1)数组大小:3n-2

(2)矩阵下标到数组下标的映射:行列相差大于1直接=0,其他的算明白是第几个元素(如果矩阵从1开始,那么前 i-1行就是3(i-1)-1个元素,a_{i,j}是i行第 j-i+2个元素,则a_{i,j}是第2i+j-2个元素)再考虑数组从0/1开始就行

(3)数组下标到矩阵下标的映射:前 i-1行 3(i-1)-1个元素,前 i行 3i-1个元素,当前元素是第k或k+1个元素(数组下标从0开始就是k+1),再解一个不等式得到i ,用k i 解 j

🐖:不等式的等号问题,要保证当前元素在第i行

        考虑当前元素是第几个,让当前元素的下标不超过i行最右

        

       考虑当前元素前面有几个元素,让前k个元素至少占满前i-1行

        

7 稀疏矩阵的压缩存储

(1)非零元素远小于矩阵元素的个数

        

(2)顺序存储--三元组法

        

(3)链式存储--十字链表法

        

        

第四章 串

4.1 串的定义和实现

1 定义

        字符串:由零个或者多个字符组成的有限序列

        

        其中位序从1开始,空串≠空格串

2 操作

3 存储结构

(1)顺序存储

        

        ①静态数组

        a. 实现

        

        b. 求子串,返回第pos个字符起长度为len的子串

        

        c. 比较操作

        字符串不同时,比较字母大小;字符串存在主串和子串,认为主串更大;完全相同=0

        

        d. 定位操作

        从头找没找到就往后一位重找,找到结束

        

        ②动态数组实现

        

(2)链式存储

        

4.2 串的模式匹配

在主串中找到和模式串相同的子串,并返回所在位置

1 朴素模式匹配算法

(1)将主串中所有长度为m的子串依次与模式串对比,直到找到一个完全匹配的子串或者所有子串都不匹配为止

(2)通过数组下标实现

        ①初始两个指针分别指向主串 i 和模式串 j 起始位置

        ②依次扫描

        ③匹配失败,i退回到往后挪一个的位置(i-j+2),j退到起始位置(j=1

        ④两种结束可能:成功——若j > 模式串长度,证明匹配成功,返回当前子串第一个字符的位置(i - 模式串长),失败——i > 主串长,该模式串不存在

(3)代码实现

        

        🐖:最坏时间复杂度 O(nm)

2 KMP算法

(1)只和模式串的内容有关,和主串内容以及主串匹配到什么位置无关,接替关键是找到模式串要回退到 j 等于几。流程:根据模式串求出next数据,利用数据进行匹配,主串的 i 不用回溯。

        🐖:最坏时间复杂度

(2)next数据:当在某一个位置发生失配时,应将模式串 j 的值修改为多少,其中第一个元素就失配,直接置零再让i j 都向后+1

        手动计算方法:next[1]=0,next[2]=1,其他在不匹配的位置前划线,移动模式串,直到能对上或全部移除线外

                e.g. next[3]=1,只用模式串就行

                        

(3)KMP进一步优化:求nextval数组,查看旧next数组,如果要指向的新位置和失配位置字符一致,直接改为要指向新位置的next值,字符不一致不用改。匹配的代码部分不用变,只需用nextval替换next数组就行

                      

第五章 数和二叉树

5.1 树的基本概念

1 概念

(1)两个结点之间的路径:只能由上往下;路径长度:经过了几条边

(2)结点的层级/深度:从上往下数;结点的高度:从下往上数

(3)结点的度:有几个孩子;树的度:各结点的度的最大值

2 性质

(1)除了根节点,任何一个结点有且只有一个前驱

(2)m叉树是指每个结点最多m个孩子,但可以不占满,与度为m的树不同

(3)度为m的树/m叉树,第i层至多有m^{i-1}个结点

(4)高度为h的m叉树至多有\frac{m^{h}-1}{m-1}个结点;高度为h的m叉树至少有h个结点;高度为h度为m的树至少有h+m-1个结点

             

(5)具有n个结点的m叉树的最小高度为

        

5.2 二叉树的概念

1 概念

(1)满二叉树:分支结点全满,含2^{h-1}个结点

         

(2)完全二叉树:编号结点与满二叉树相同,可以去掉编号大的一部分

         

(3)二叉排序树:左子树关键字<根节点关键字<右子树关键字

        

(4)平衡二叉树:任意结点左子树和右子树的深度之差不超过1。一般是在二叉排序树基础上调平衡,可以提高查找效率

        

2 性质

(1)有序树,左右子树不能颠倒

(2)n_{0} = n_{2} + 1

(3)二叉树第i层至多有2^{i-1}个结点

(4)高度为h的二叉树至多有2^{h}-1个结点

(5)具有n个结点的完全二叉树的高度h为\left \lceil log_{2}(n+1) \right \rceil 或 \left \lfloor log_{2}n \right \rfloor + 1

                    

(6)对于完全二叉树可以算出n_{0} n_{1} n_{2}

3 二叉树的存储结构

(1)顺序存储:从左到右从上到下存储完全二叉树中结点,可以用数组下标找元素

                       

        

        🐖:对于非完全二叉树,可以按照对应位置存,没有的元素存空,但判断是否有左右孩子时只能看isempty字段,不能按照位置和n的大小关系看

(2)链式存储

        ①定义

        

        ②初始化

        🐖:找左右孩子看左右指针,找父结点从头遍历(或用三叉链表)

5.3 二叉树的遍历和线索二叉树

1 三种遍历方法

根据根的位置确定遍历顺序

(1)先序遍历

        

(2)中序遍历

        

(3)后序遍历

        

2 三种遍历的应用

        求树的深度

        

3 层序遍历

(1)辅助队列,队列非空队头出队并访问,将其左右孩子入队

(2)实现

        

4 由遍历序列构造二叉树

(1)前+中

        在前序从左到右找到根,在中序中找左右子树

(2)后+中

        在后序从右到左找到根,在中序中找到左右子树

(3)层+中

        在层序中找到每一层的根,在中序中找左右子树

🐖:一定要有中序遍历

5 线索二叉树

(1)普通二叉树:遍历必须从根开始,需要找指定结点的遍历顺序上的前驱和后继时可以借助(pre q指针),线索二叉树:将左空指针指向前驱,右空指针指向后继,可以实现从某个点开始的遍历

(2)存储结构:tag=0表示指向孩子,tag=1表示指向线索

        

(3)代码实现二叉树的线索化

        ①中序线索化

        ,p指针是随着指左孩子右孩子自动变的

         ,这里注意一下连右指针是看pre

        看当前结点的左孩子空不空,用来连前驱,再看前驱结点的右孩子空不空,用来连后继。

        

        

        ②先序线索化

        

        

        

        🐖:先序线索化有转圈圈问题!!!要判断当前左孩子是不是线索,不是线索再对其进行处理,这里多理解一下为啥中序不用

        

        ③后序线索化

        

        

        

        

        🐖:后序线索化已经把当前结点的左右子树处理完了,所以也不存在转圈圈!!!

(4)在线索二叉树中找前驱和后继

        找后继看rtag,找前驱看ltag

                

        ①中序线索二叉树找中序后继

                p->rtag==1,next=p->rchild;p->rtag==0,next=右子树中最左下角的结点

                

                可以用这种找后继的方法实现不用递归的中序遍历

                

        ②中序线索二叉树找中序前驱

                p->ltag==1,pre=p->lchild;p->ltag==0,pre=左子树中最右下角的结点

                

                可以用这种找前驱的方法实现不用递归的逆中序遍历

                

        ③先序线索二叉树找先序后继

                p->rtag==1,next=p->rchild;p->rtag==0,next=有左孩子为左孩子,没左孩子为右孩子

        ④先序线索二叉树找先序前驱

                p->ltag==1,pre=p->lchild;p->ltag==0,找不到前驱

                🐖:如果用pre q找到当前结点的父结点,再找前驱有三种可能 / 或者当前结点是根节点则没有前驱

                     

        ⑤后序线索二叉树找后序后继

                p->rtag==1,next=p->rchild;p->rtag==0,找不到后继

                🐖:如果用pre q找到当前结点的父结点,再找后继有三种可能 / 或者当前结点是根节点则没有后继

                

        ⑥后序线索二叉树找后序前驱

                p->ltag==1,pre=p->lchild;p->ltag==0,pre=有右孩子为右孩子,没右孩子为左孩子

5.4 树、森林

1 树的存储结构

(1)顺序存储

        按照完全二叉树中结点顺序存储,用数组下标表示结点之间的逻辑关系,没有元素的位置空

(2)双亲表示法

        顺序存储所有结点,非根结点的双亲指针指向父节点在数组中的下标,根结点==-1

                

        找父节点方便(并查集),找孩子需要遍历整个数组

(3)孩子表示法

        顺序存储所有结点,每个结点中保存数据元素和孩子链表头指针,用链表记录一个结点的孩子编号

                

        找孩子结点方便(服务流程树),找父节点需要遍历整个链表

(4)孩子兄弟表示法

        二叉链表,每个节点保存数据元素和两个指针,第一个指针指向第一个孩子,第二个指针指向当前结点右兄弟,存储森林时将每个根节点视为同级兄弟关系

2 树、森林与二叉树的转换

(1)树转二叉树

        如果当前结点有孩子,将所有孩子穿起来,并将第一个孩子连到当前结点左边,依次处理所有结点

        

(2)森林转二叉树

        将森林所有树根节点穿起来,按层处理所有树的同一层结点,处理方法同树转二叉树

(3)二叉树转树

        先找到根节点,然后找左指针指向的结点,以及该结点右指针指向的一串结点,这些结点都是上一层的孩子节点,依次处理所有结点

(4)二叉树转森林

        先把每个树拆出来,再按照二叉树转树做

3 树和森林的遍历

(1)树的遍历

        ①先根遍历

        若树非空先访问根节点,再依次对子树进行先根遍历

        

        先根遍历和将树转换为对应二叉树的先序序列相同

        ②后根遍历

        若树非空则对每个子树进行后根遍历,最后访问根节点

        

        后根遍历和将树转换为对应二叉树的中序序列相同

        ③层序遍历

        借助队列,根节点入队,出队队头元素并入队其孩子

(2)森林的遍历

        ①先序遍历:依次对各个子树进行先根遍历 / 将森林转成二叉树进行先序遍历

        ②中序遍历:依次对各个子树进行后根遍历 / 将森林转成二叉树进行中序遍历

5.5 树和二叉树的应用

1 哈夫曼树

(1)结点的带权路径长度:从树的根到该结点经过的边数和该结点权值的乘积

        树的带权路径长度:树中所有叶结点的带权路径长度之和

        哈夫曼树是带权路径长度最小的二叉树

(2)构造:依次选择权值小的结点

(3)特点:每个初始结点最终都是叶结点,结点总数2n-1,不存在度为1的结点,哈夫曼树不唯一但WPL都相同

(4)应用:哈夫曼编码,允许对不同字符用不等长的二进制位表示的可变长编码,用于对数据进行压缩

2 并查集

(1)逻辑结构:集合,用互不相交的树表示多个集合

(2)存储结构:双亲表示法,用一个数组表示集合关系

(3)基本操作

        查:某个元素属于哪一个集合,从指定元素出发,找到根节点;两个元素是否为同一个集合,判断根节点是否相同

        并:和并两个集合,让一个树成为另一个树的子树

(4)代码实现

        ① 初始化

        

        

        ② 并、查

        

        时间复杂度:查最坏O(n),并O(1)

(5)并操作的优化

        用根节点的绝对值表示树的结点总数,小树向大树和并,可以使得树高不超过 \left \lfloor log_{2}n \right \rfloor+1,查操作最坏时间复杂度变为O(log_{2}n)

        

        

(6)查操作的优化 / 压缩路径

        先找到根节点,再将查找路径上所有结点都挂到根节点下

        

第六章 图

6.1 图的基本概念

G = {v,E},图不能为空,V一定是非空集

无向图,(v,w) = (w,v)表示同一条边,全部顶点的度的和=边数的2倍

有向图,<v,w>表示从弧尾v指向弧头w的一条弧,全部顶点的入度和=全部顶点的出度和=边数

路径:顶点之间到达的顶点序列,无向图/有向图都可能存在顶点之间不存在路径

点到点的路径:顶点之间的最短路径,不存在时为无穷

连通图(无向图):任意两个顶点之间连通,G连通图最少要n-1条边,G非连通图最多有C_{n-1}^{2}条边

强连通图(有向图):任意两个顶点之间强连通,G是强连通图,最少n条边

生成子图:包含所有结点的子图

连通分量(无向图):极大连通子图(子图必须连通且包含尽可能多的顶点和边)

强连通分量(强连通分量):极大强连通子图

生成树:连通图中包含全部顶点的极小连通子图

生成森林:非连通图中连通分量的生成树构成的森林

带权图:边上带有权值的图

带权路径长度:一条路径上所有边的权值之和

无向完全图:无向图中两个顶点之间都存在边,边数 C_{n}^{2}

有向完全图:有向图中两个顶点之间存在方向相反的弧,边数 2C_{n}^{2}

树:不存在回路且连通的无向图,且边数为n-1,若大于n-1则必存在回路,注意前面的结论是树的森林不适用

有向树:一个顶点的入度为0,其余顶点的入度为1的有向图

6.2 图的存储及基本操作

1 邻接矩阵法

(1)二维数组存边,一维数组存顶点

(2)求结点度

        无向图:一行或一列

        有向图:出度看行入度看列

        时间复杂度:O(V)

(3)空间复杂度O(V^{2}),只和顶点相关,和实际边数无关

(4)邻接矩阵相乘:A^{n}中的元素A^{n}[i][j]等于从顶点 i 到顶点 j 长度为n的路径数目

2 邻接表法

(1)各个节点顺序存储,再用链表存放相邻结点

有向图类似

        

(2)求结点度

        无向图:遍历结点指针连着的链表

        有向图:出度遍历结点边链表,入度遍历所有边链表找结点

(3)空间复杂度

        无向图:O(V+2E),有向图O(V+E)

(4)邻接表表示不唯一,邻接矩阵表示唯一

3 十字链表(存有向图)

(1)十字链表存有向图

        顺着绿色指针找可以找到所有弧尾相同的边,即从该结点发出的弧

        顺着橘色指针找可以找到所有弧头相同的边,即指向当前结点的弧

        

(2)结点的度

        出度找绿色,入度找橘色

(3)空间复杂度:O(V+E)

(4)解决了邻接矩阵空间复杂度高的问题,也解决了邻接表找入度必须遍历整个链表的问题

4 邻接多重表(存无向图)

(1)邻接多重链表存无向图

        横向的是边中橘色结点相同,纵向的是边中绿色结点相同

        

(2)删除某条边顺着原本的链表指针走向修改指针,删除某个结点需要删除掉所有相关的边并修改指针

(3)解决了邻接矩阵空间复杂度高的问题,解决了邻接表中删除边和结点不变以及边存储冗余

5 图的基本操作

(1)判断边或者弧是否存在

        看邻接矩阵中对应下标元素是1还是0,时间复杂度O(1);在邻接链表中找,时间复杂度最坏O(V)

(2)列出和结点相邻的边

        无向图:邻接矩阵遍历这个结点的这一行或这一列,时间复杂度O(V);邻接链表遍历该结点指向的链表,时间复杂度最坏O(V)

        有向图:出边行入边列,时间复杂度O(V),出边该结点指向的链表最坏O(V),入边遍历所有边,时间复杂度O(E)

(3)插入新顶点

        在存结点的顺序表末尾加入新结点即可,时间复杂度O(1)

(4)删除某个结点

        无向图:邻接矩阵将该顶点的行列全部置0,并在结点顺序表中标记该结点为空结点,时间复杂度O(V),邻接表将顶点置空,并将指针指向的链表删除,并遍历整个链表删除相同的边,时间复杂度最坏O(E)

        有向图:邻接矩阵同无向图,邻接表删除该顶点的出边只需删除指针指向的链表,时间复杂度O(V),删除顶点的入边需要遍历整个链表O(E)

(5)在两个结点之间插入一条边

        邻接矩阵将对应元素置1,邻接表采用头插在边表中进行新边的插入,时间复杂度O(1)

(6)找到与某个结点相邻的第一个结点

        无向图:邻接矩阵找到该行的第一个元素1,时间复杂度最坏O(V),邻接表找到结点边表中的第一个结点,时间复杂度O(1)

        有向图:邻接矩阵出边行入边列,时间复杂度最坏O(V),邻接表出边边表中第一个结点,时间复杂度O(1),入边遍历整个边表,时间复杂度最坏O(E)

(7)接着找顶点的下一个邻接点

        邻接矩阵找该行下一个元素1,时间复杂度最坏O(V),邻接表找结点边表中第二个结点,时间复杂度为O(1)

(8)获取或设置指定边的权值

        核心是找,看邻接矩阵中对应下标元素是1还是0,时间复杂度O(1);在邻接链表中找,时间复杂度最坏O(V)

6.3 图的遍历

1 广度优先遍历BFS

(1)在标记数组中找false元素,并将未访问的相邻元素标记并入队

(2)空间复杂度最坏O(V),时间复杂度邻接矩阵存储O(V^{2}),邻接表存储O(V+E)

(3)广度优先生成树,只保留广度优先遍历的路径;广度优先生成森林,对每棵树分别广度优先遍历路径

2 深度优先遍历DFS

(1)在标记数组中找false元素进行访问和标记,找与该节点相邻的结点,并顺着该节点继续进行访问

(2)空间复杂度最坏O(V);时间复杂度邻接矩阵存储O(V^{2}),邻接表存储O(V+E)

(3)深度优先生成树,只保留深度优先遍历的路径;深度优先生成森林,对每棵树分别深度优先遍历路径

(4)图的遍历和连通性:对无向图进行BFS/DFS遍历,调用次数=连通分量数,对于有向图要看该节点是否有到全部结点的路径

6.4 图的应用

1 最小生成树

(1)生成树,包含所有结点,n-1条边,结点之间连通。最小生成树,带权连通图中权值之和最小的生成树,当连通图本身就是树时,最小生成树就是它本身,非连通图只有生成森林

(2)Prim求最小生成树

从某个顶点开始创建,每次将代价最小的新顶点纳入

时间复杂度O(V^{2}),适合边稠密图

(3)Kruskal求最小生成树

每次选择权值最小的边,使两头连通

时间复杂度O(Elog_{2}E),适合边稀疏图

2 最短路径

(1)单源最短路径,从一个顶点出发到其他任意顶点最短距离;每对顶点之间的最短路径

(2)BFS,单源无权

        用数组存储路径长d[ ]和前驱节点path[ ],借助队列存储相邻未访问结点并修改访问数组和路径长度和前驱结点数组

        

                

        

(3)Dijkstra,单源带权无权

        初始化数组,各个顶点之间是否已经找到最短路径final[ ]和最短路径长度dist[ ]和路径上的前驱path[ ]。循环遍历结点,选出还没确定最短距离的当前最小的dist[ ]作为下一个顶点,并标记该顶点已经找到最短路径,修改借助该顶点的dist[ ]和path[ ],重复操作

        时间复杂度O(V^{2})

🐖:Dijkstra不适合有负权值的带权图

(4)Floyd,各顶点间带权无权

        动态规划,将最短路径分为需不需要其他顶点,需要几个顶点考虑。初始化数组A和数组path存不需要借助顶点的最短路径和中转点,依次加入中转点修改两个数组

                

        

        

        时间复杂度O(V^{3}),空间复杂度O(V^{2})

🐖:Floyd可以解决负权值带权图,不能解决带有负权回路的图

3 有向无环图描述表达式

(1)有向图中不存在环,即DAG图

(2)用有向无环图描述表达式,去掉相同的部分。

        

                

🐖:当用到运算结果时就需要再向上一层,当运算符的两个操作数相同时可以和并运算符

4 拓扑排序

(1)用DAG图表示一个工程,顶点表示活动,即AOV网

(2)拓扑排序手动实现

        

(3)拓扑排序代码实现

        用栈保护入度为0的结点,依次出栈输出并将其相邻的结点的入度 -1

        

        时间复杂度邻接矩阵存储O(V^{2}),邻接表存储O(V+E)

(4)逆拓扑排序手动实现

        

(5)DFS实现逆拓扑排序

        当该结点没有未访问过的相邻节点时输出该结点

第七章查找

7.1 查找的基本概念

1 查找算法的评价指标

(1)查找长度,需要对比关键字的次数。

(2)平均查找长度ASL,关键字比较的平均值,当题目没指明关键字的查找概率时,视为每个关键字查找概率相同

7.2 顺序查找和折半查找

1 顺序查找

(1)思想,通常用于线性表,依次查询

(2)实现

        

        带哨兵的实现方法,将要查找的元素存在0号位置,从后向前依次查找,返回查找到的数组下标,当返回0时说明查找失败,无需判断是否越界,效率更高。

        

        

(3)查找效率分析

        

        时间复杂度O(n)

(4)对有序表用查找判定树优化查找

        对查找失败优化明显

        成功结点的查找长度=自身层数,失败结点的查找长度=父节点所在层数

        

(5)被查概率不同时的优化

        将查找结点概率高的排在前可以优化查询成功时的ASL

2 折半查找

(1)思想,只适用于有序的顺序表(链表没有随机存取的特性找中间元素困难),初始用两个指针指向头尾,取中间元素进行比较并根据大小缩短区间,重复操作

(2)实现

        

(3)查找效率分析

        

        

        时间复杂度O(log_{2}n)

(4)折半查找判定树的构造

        右子树最多比左子树多一个元素,一定是平衡二叉排序树,只有最下面的一层是不满的,树高(不含失败结点)h=\left \lceil log_{2}(n+1) \right \rceil,失败结点n+1个

        

🐖:折半查找并非一定比顺序查找更快,当查询元素在第一个时顺序更快

        求mid时也可以向上取整

        

3 分块查找

(1)思想,对数组进行分块,用索引表保存每个分块的最大关键字和分块的存储区间。块内无序块间有序,先在索引表中找到查找元素属于哪一块(可顺序可折半),再进行顺序查找

        

当索引表使用折半查找且查找元素没在索引表中时,在low>high之后要在low所指块中顺序查找

        

当low指空时查找失败

        

(2)查找效率分析 

        算所有元素查找时需要对比几次关键字,注意索引表是顺序查找还是折半查找,折半查找若查找元素不是索引表中的元素,需要low>high才能确定在那个分块

        

        当长度为n的索引表被均匀的分成b块,每块s个元素时平均查找长度计算如下,当将数组分成\sqrt{n}块每块中\sqrt{n}个元素时ASL最小

        

        

(3)

        

7.3 树形查找

1 二叉排序树BST

(1)定义,左子树<根节点<右子树,中序遍历可得到递增序列

(2)查找,对比当前结点和根节点的大小,小于朝左大于朝右

        非递归实现和递归实现

        

(3)插入,先用查找操作找到插入位置,因为要修改指针所以传入的是引用类型,不允许插入相同节点

        递归实现时间复杂度O(h)

(4)构造,不断插入新节点

(5)删除,先找到目标结点

        ① 删除结点是叶节点,直接删

        ② 删除结点只有左子树或只有右子树,用子树替代结点位置

        ③ 删除结点左右子树都有。用右子树最左下的结点代替(一定没有左子树,转成①/②情况),并删除最左下结点。用左子树最右下的结点代替(一定没有右子树,转成①/②情况)

(6)查找效率分析

        ① 查找长度=对比关键字次数,查询效率受树高影响,n个结点的二叉树树高最小\left \lfloor log_{2}n \right \rfloor+1,平均查找长度O(log_{2}n)。树高最大n,平均查找长度O(n)

        ② 查找成功的平均查找长度

        

        ③ 查找失败的平均查找长度

        

2 平衡二叉树AVL

(1)定义,树上任意结点的左子树和右子树的高度差不超过1

        

(2)调整最小不平衡树

        ① LL,B右旋代替A

        ② RR,B左旋代替A

        ③ LR,C左旋代替B,再右旋代替A

        ④ RL,C右旋代替B,再左旋代替A

(3)查找效率分析

深度为h的平衡二叉树中含有的最少结点数计算公式n_{h}=n_{h-1}+n_{h-2}+1

n_{0}=0n_{1}=1n_{2}=2n_{3}=4n_{4}=7n_{5}=12...

含有n个结点的平衡二叉树的最大深度为O(log_{2}n),平均查找长度为O(log_{2}n)

(4)删除

        ① 用同二叉排序树的方法删除结点

        

        ② 向上找最小不平衡子树,找不到删除结束

        ③ 当存在最小不平衡子树时,找其最高的儿子和孙子

        

        ④ 根据孙子相对于最小不平衡结点的位置,按照插入调整(LL RR LR RL)的方法调整

        

        ⑤ 当不平衡向上传导时,继续②

3 红黑树RBT

(1)插入删除很多时候不会破坏红黑特性,无需频繁调整树的形态,适用于频繁插入删除的场景

(2)定义性质,红黑树是二叉排序树,叶子节点是查找失败的结点不是元素结点。左根右,根叶黑,不红红,黑路同

        

        

        

(3)查找,和二叉排序树相同

(4)插入,先找位置,黑叔看位置,红叔直接染

        

                

        

(5)删除

        

(6)黑高,从某个结点出发(不含该节点)到达任一空叶结点的路径上的黑结点总数

        内部结点最少,黑高为h的情况,树为满树形态全为黑节点,节点数最少2^{h}-1

         

7.4 B树和B+树

1 B树

(1)定义,多路平衡查找树,注意子树数目和绝对平衡

        

(2)B树的高度,计算高度不包括失败节点层

        

        

(3)插入,新元素从终端节点进行插入,通过查找找插入位置

        插入关键字导致溢出时,从\left \lceil m/2 \right \rceil的位置进行分裂,将中间结点提到父结点时注意按顺序

                                                    

 

(4)删除

① 删除终端节点,且删除后原结点关键字个数仍合法,直接删除

删60

② 删除非终端节点,用直接前驱(左子树中 最右下的结点)/ 用直接后继(右子树中最左下的结点),转换成删除终端结点

删80

③ 删除终端结点后,关键字数低于下限

删38

删90

删49

2 B+树

(1)定义

(2)查找

        ① 从根节点查,B+树查找需要一直找到最下面一层的叶子节点中对应的元素

        ② 在叶子节点中顺序查找

(3)B树和B+树的区别

BB+
关键字n-1,分叉n关键字n,分叉n

根节点关键字[1,m-1]

其他节点关键字[\left \lceil m/2 \right \rceil-1,m-1]

除根节点外,最少\left \lceil m/2 \right \rceil个分叉

根节点关键字[1,m]

其他节点关键字[\left \lceil m/2 \right \rceil,m]

除根节点外,最少\left \lceil m/2 \right \rceil个分叉

各节点关键字不重复非叶结点中出现的关键字也会出现在叶子节点中
所有结点中包含记录只有叶子节点包含记录,非叶结点仅起索引作用,树更矮,读磁盘次数更少

7.5 散列表 

1 基本概念 

(1)散列表,数据结构,可以根据数据元素的关键字计算出在散列表中的存储地址

(2)散列函数,关键字到存储地址的映射

(3)冲突,存储地址已经存储了其他元素

(4)同义词,不同关键字通过同一个散列函数映射到同一个存储地址

2 散列函数的构造

(1)除留余数,散列表表长m,取不大于m但最接近或等于m的质数p(对质数取余分布更均匀)

(2)直接定址,H(key) = key / H(key) = a*key + b,适用于关键字分布基本连续,计算简单不会产生冲突,但当关键字分布不连续时易造成空间浪费

(3)数字分析,选取数码分布均匀的若干位作为散列地址,适用于关键字集合已知,且某几位数码位分布均匀

(4)平方取中,取关键字平方值的中间几位作为散列地址,适用于关键字每位取值都不够均匀

3 解决冲突的方法

(1)拉链法,将所有同义词存储在一个链表中。

        ① 插入,先计算地址再用头插或尾插。

🐖:插入时如果保持链表有序,可以略微提高查找效率

        ② 查找,先计算地址,再顺着链表查,注意计算查找长度是关键字对比次数,空指针不算

        ③ 删除,先计算地址,再顺序访问链表,删除相应结点

(2)开放定址法,在发生冲突时,给新元素找另一个空闲位置,核心在于怎么确定偏移量,注意每次探测新位置都是用初始位置+偏移量

        

        ① 线性探测

        插入,先找到初始散列地址,如果发生冲突依次往后找,直到找到空位置插入

        查找,先找到初始散列地址,如果对比失败依次向后对比,直到找到或遇到空指针

        ② 平方探测

        插入,先找初始散列地址,如果发生冲突,以±1 ±2..偏移进行位置查找

        查找,找位置同插入,直到关键字对比成功或遇到空指针结束

        ③ 双散列,再给出一个散列函数用来计算偏移值d,插入和查找

        ④ 伪随机序列,偏移量由随机序列给出,插入和查找

        ⑤ 删除元素,通过进行特殊标记实现逻辑删除。当逻辑删除的元素过多时需要进行整理。逻辑删除的结点可以插入新元素

        ⑥ 探测覆盖率

        线性探测可全覆盖

        ,其他长度采用平方探测至少能探测到一半位置

        

        伪随机序列法能否探测到所有位置取决于序列设置

第八章 排序

8.1 排序的基本概念

(1)使元素有序

(2)关注时间、空间复杂度和算法稳定性(看相同关键字的相对位置)

(3)内部排序,数据都在内存。外部排序,数据太多无法全部放入内存

8.2 插入排序

1 直接插入排序

(1)每次将待排序的记录按关键字大小插入前面已经排好序的子序列

 

(2)实现

        ① 待排元素小于前面元素时,需要将已排好的元素后移

        ② 带哨兵的实现方法,将待排元素存入A[0]位置,A[0]小于前面元素时,需要将已排好的元素后移,不用判断数组越界

(3)算法效率分析

        ① 空间复杂度O(1)

        ② 时间复杂度,来自关键字对比和移动元素。

        最好情况为原本有序O(n),n-1趟每趟对比一次关键字,不用移动

        最坏情况为原本逆序O(n^{2}),n-1趟每趟对比i+1次关键字,移动i+2次关键字

        平均时间复杂度O(n^{2})

        ③ 算法稳定性,稳定

(4)优化,在前面已经有序的序列中用折半查找插入位置

        ① 当high<low时,low指针所指向的元素及其以后的元素全部后移,并将A[0]复制进low指向的位置

        

        ② 实现

        ③ 减少了关键对比次数,但移动次数没变,时间复杂度O(n^{2})

(5)也可以对链表进行直接插入排序,但只能对已排好序列用顺序查找查找待排元素的位置,移动元素次数减少,时间复杂度O(n^{2})

2 希尔排序

(1)定义,将排序表划分成子表,对各个子表直接插入排序,缩小增量重复操作

        

        

        

(2)实现

         

(3)性能分析

        ① 空间复杂度O(1)

        ② 时间复杂度,受增量影响,最坏O(n^{2}),最好O(n^{1.3})

        ③ 稳定性,不稳定

        ④ 只适用于顺序表

8.3 交换排序

1 冒泡排序

(1)两两对比把更小的往前放(增序),每轮可以确定一个元素的位置,已经确定位置的元素下轮不参与对比,当一趟中没有进行交换时,已经排好序了

(2)实现

(3)性能分析

        ① 空间复杂度O(1)

        ② 时间复杂度

        最好有序O(n),比较n-1,不交换

        最坏逆序O(n^{2}),比较=交换=\frac{n(n-1)}{2},移动次数=3*交换次数

        平均O(n^{2})

        ③ 稳定性,稳定

        ④ 适用于顺序表、链表

2 快速排序

(1)定义,选择基准元素,将小于该元素的放到左边,大于等于该元素的放到右边,实现对表的划分,再分别处理基准左右两边子表,当移动完的左/右子表中只有一个元素时,该子表不用处理,每次至少可以确定一个中间元素位置

        放元素的过程通过两个指针实现,high指向的元素小于基准时就放到low指向的位置,然后交换low指针移动,low指针+1指向的元素大于基准时就放到high指向的位置,交换high指针移动,元素不用移动时,直接low向后或high向前。当low==high时,在此位置存入基准元素

        

        

(2)实现

需要借助工作栈来保存每次的子表处理

(3)效率分析

        递归层数同二叉树,最好\left \lfloor log_{2}n \right \rfloor+1,最坏n

        ① 空间复杂度O(递归层数) 

        ② 时间复杂度O(n*递归层数)

        ③ 稳定性,不稳定

(4)优化

        选基准时,取头中尾中的中间那个或随机选一个

(5)一趟和一次划分

        

8.4 选择排序

1 简单选择排序

(1)定义,每趟在待排序元素中选取关键字最小的元素加入有序序列,相同关键字取第一个

🐖:直接插入是在有序序列中找位置,简单选择是在待排序列中找元素

             

(2)实现

        

(3)性能分析

        ① 空间复杂度O(1)

         ② 时间复杂度O(n^{2}),不受初始状态影响,n-1趟,对比\frac{n(n-1)}{2}次,交换<n-1

        ③ 稳定性,不稳定

        ④ 适用于链表、顺序表

2 堆排序

(1)大根堆:根≥左右子树,小根堆:根≤左右子树

(2)建立大根堆:从最后一个非终端节点(i ≤ \left \lfloor n/2 \right \rfloor)向前检查一遍所有非终结点并进行调整,调整时与自己更大的孩子互换,如果破坏了下层的大小关系,将该元素继续进行下坠

(3)建立大根堆的实现

                

        

(4)基于大根堆进行排序,每一趟将堆顶元素加入有序子序列,并将需要排序的数组长度-1(与待排序列最后一个元素交换)将待排序列再调整为大根堆(小元素下坠)

(5)堆排序实现

        

(6)效率分析

        ① 空间复杂度O(1)

        ② 建堆的时间复杂度分析,每下坠一层最多对比关键字2次,i层的结点下坠最多对比关键字2(h-i)次。n个结点,每层最多2^{i-1}个结点,树高h=\left \lfloor log_{2}n \right \rfloor+1

        建堆时间复杂度O(n)

        ③ 完整堆排序时间复杂度分析,

        

        

        ④ 稳定性,不稳定 

(7)插入,将插入元素放在表尾,与父节点对比,逐层上升

(8)删除,用堆底元素替代删除元素,然后逐层下坠

🐖:注意关键字对比次数:下坠对比一次(只有一个孩子)或两次(两个孩子),上升对比一次

8.5 归并排序、基数排序

1 归并排序

(1)定义,将两个或多个已经有序的序列合并成一个,m路归并每选出一个元素需要对比关键字m-1次

        

(2)二路归并排序手算实现

        

(3)代码实现

        先将待排序序列复制进长度相同的辅助数组,i,j指针指向数组B中的两个有序序列头,k指针指向A头,依次选择更小的数组赋值进数组A

(4)效率分析

        ① 空间复杂度O(n),辅助数组B,递归中产生的空间小于n数量级不计

        ② 时间复杂度

         

        ③ 稳定性,稳定

2 基数排序

(1)定义,将元素进行分配(从队尾进)和收集(从队头出)

        关键字每一位的取值范围个数叫基数r,e.g.0~9 r=10

        根据取值范围设置队列,根据位数确定扫描趟数

(2)效率分析

        

        ① 空间复杂度O(r)

        ② 时间复杂度O(d(n+r)),分配扫结点总数,收集扫取值范围决定的链表

        ③ 稳定性,稳定

(3)应用

        

        

8.6 各种内部排序算法的比较及应用

8.7 外部排序

1 外部排序

(1)内外存之间的数据交换,磁盘的读写以块为单位,读入内存后才能修改,修改完要写回磁盘

(2)外排原理,对磁盘中的数据进行排序

构造初始归并段

🐖:写回内存是写到另一片空间,原空间会归还给系统

和并两个段,先取两个中小的部分,凑出一个块就输出,当输入缓冲区中有一个全部空了用剩下大的部分的立刻补上

(3)时间开销

        外部排序时间开销 = 读写外存的时间+生成初始归并段时内部排序的时间+内部归并两个段时间,其中主要开销来自读写外存的时间【(文件总块数*2+文件总块数*2*归并趟数)*一次的时间】

(4)优化

        

        ① 在内存中增加输入缓冲区来减少归并趟数

        ② 生成初始归并段的内存工作区越大,归并段的数量越少

2 败者树 

(1)定义,在内存中归并时减少关键字对比次数

(2)实现

构建好败者树后,选出最小元素只需对比\left \lceil log_{2}k \right \rceil

3 置换-选择排序

(1)定义,让初始归并段的数量尽可能的少,且初始归并段的长度可以超过内存工作区

(2)实现

🐖:记录输出时先放到输出缓冲区中,一块满了放到磁盘中,磁盘读入也是以块读入的,然后以记录读入内存工作区

4 最佳归并树

(1)二路归并的最佳归并树,哈夫曼树

(2)多路归并树

        ① 当初始归并段数是归并路数k的整数倍时和二路归并同理

        ② 当初始归并段数不是整数倍时,需要补充几个长度为0的虚段

       

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值