OUC中国海洋大学数据结构2023期末复习

第一章 绪论

基本概念和术语

数据

输入到计算机中,且能被计算机处理的符号的总称

数据元素

数据结构中讨论的基本单位

数据项

最小单位。数据元素(运动员)可以是数据项(年龄等)的集合

数据对象

性质相同的数据元素的集合

数据结构

带结构的数据元素的集合,相互之间存在着某种逻辑关系的数据元素的集合。

数据结构的形式定义为一个二元组(D,S)

其中D是数据元素的有限集、S是D上的关系集

数据的逻辑关系

线性结构

有唯一前驱和后继

树形结构

一对多

图状结构或网状结构

多对多

集合结构

没关系

图书馆的书目检索系统自动化问题——线性结构

计算机和人下棋(决策)——树形结构

多叉路口交通灯的管理问题——图

数据的存储关系

逻辑结构在存储器中的映象

顺序映像

以相对的存储未知表示后继关系。整个存储结构中只含数据元素本身的信息

链式映像

以附加信息(指针)表示后继关系,需要用一个和x在一起的附加信息指示y的存储位置

抽象数据类型

简称ADT,Abstract Data Type

两个重要特征

数据抽象、数据封装

描述方法

(D、S、P)三元组

其中D是数据对象、S是D上的关系集、P是对D的基本操作集

算法和算法分析

算法的五个重要特性

有穷性

合法输入、有穷步骤后一定能结束。算法中的每个步骤都能在有限时间内完成

确定性

每种情况下所应执行的操作,在算法中都有确切的规定

任何条件下、算法都只有一条执行路径、不允许有二义性

可行性

所有操作足够基本,不要瞪眼法可得

有输入

有输出

算法设计的原则

正确性、可读性、健壮性、高效率于低存储量需求

算法效率的度量

时间复杂度、空间复杂度

如何计算时间复杂度

第二章 线性表

了解线性表的的逻辑结构特性,以及顺序表和链表

线性结构的基本特征

  • 集合中必定存在一个唯一的“第一元素”
  • 集合中必定存在唯一的一个“最后元素”
  • 除最后元素在外,均有 唯一的后继
  • 除第一元素之外,均有 唯一的前驱

操作集

算法 A U B

GetElem(LB, i)->e 从线性表LB中依次察看每个数据元素

LocateElem(LA,e,equal()) 依值在线性表LA中进行查访

ListInsert(LA,n+1,e) 若不存在就插入

void union(List &La, list Lb){
    La_len = ListLength(La);
    Lb_len = ListLength(Lb);
    for(int i=1; i<=Lb_len; i++){
        GetElem(Lb,i,e);
        if(!LocateElem(La,e,equal()))
            LinstInsert(La,++La_len,e);
    }
}

要先增加长度,再传递参数

时间复杂度O(Len_A * Len_B)

算法 取不重复的元素

和上一个算法一样,只需要La是空的就可以了

算法 合并有序数组

(归并排序)

void Mergelist(List La, List Lb, List &Lc){
	InitList(Lc);
    La_Len = ListLength(La);
    Lb_Len = ListLength(Lb);
    int i = j = 1;
    int k = 0;
    //先比较两个数组,谁小谁插进去
    while((i<=La_Len)&&(j<=Lb_Len)){
        GetElem(La,i,ai);
        GetElem(Lb,j,bj);
        if(ai<=bj){
            ListInsert(Lc,++k,ai);
            ++i;
        }
        else{
            ListInsert(Lc,++k,ai);
        }
    }
    //可能有一个数组先处理完
    while(i<=La_Len){
        GetElem(La, i++, ai);
        ListInsert(Lc,++k,ai);
    }
    while(k<=Lb_Len){
        GetElem(Lb,j++,bj);
        ListInsert(Lb,++k,bj);
    }
}

掌握线性表的顺序表示和实现

顺序表

用一组地址连续的存储单元,依次存放线性表中的数据元素

线性表的起始地址称作线性表的基地址

可以实现随机存取:直接操作、直接存储、直接定位

顺序表的C语言描述

#define LIST_INIT_SIZE	80
#define LISTINCREAMENT	10

typedef struct{
    ElemType *elem;	//存储空间基址
    int length;		//当前长度
    int listsize;	//当前分配的元素个数
}SqList;

Status InitList_Sq(SqList& L){
    L.elem = (ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
    if(!L.elem)	exit(OVERFLOW);
    L.length = 0;
    L.listsize = LIST_INIT_SIZE;
    return OK;
}

// 插入算法,时间复杂度O(ListLength(L))
Status ListInsert_Sq(SqList &L, int i, ElemType e){
    if(L.length >= L.listsize){
        L.base = (Elemtype*)realloc((listsize+LISTINCREAMENT)*sizeof(ElemType));
        if(!L.base)	exit(OVERFLOW);
        L.listsize += LISTINCREAMENT;
    }
    if(i<=0 || i>L.length+1){
        return ERROR;
    }
    q = &(L.elem[i-1]);	//q是插入位置
    for(p = &(L.elem[L.length-1]); p>=q; --p){
        *(p+1) = *p;	//插入位置以及以后的元素后移
    }
    *q = *e;		//插入e
	 ++L.length;	
    return OK;
}

//删除算法
Status ListDelete_Sq(SqList &L, int i/*, ElemType &e*/){
    if((i<1 || i>L.length))	return ERROR;
    p = &(L.elem[i-1]);			//p为被删除元素的位置
    //e = *p;						//被删除元素的值赋给e
    q = L.elem + L.length - 1;	//表尾元素的地址
    for(++p; p<=q; p++){
        *(p-1) = *p;
    }
    --L.length;
    return OK;
}

//查询第一个满足判定条件的数据元素
int LocateElem_Sq(SqList L, ElemType e, Status(*compare)(ElemType, ElemType)){
    i = 1;
    p = L.elem;
    //p 和 i是同步增加的,跳出循环两种可能,一种走到头了,另一种是找到了
    while(i<=L.length && !(*compare)(*p,e)){
        p++;
        ++i;
    }
    if(i<=L.length)	return i;
    else return 0;
}

考虑插入时移动元素的平均情况

n是顺序表长度,i是(索引+1)

在第i个元素之前插入元素,需要移动n-i+1个元素

若假定在线性表中任何一个位置上进行插入的概率都是相等的,则移动元素的期望值为:

image-20250620112938314

考虑删除时移动元素的平均情况

删除第i个元素,需要移动n-i个元素

若假定在线性表中任何一个位置上进行删除的概率都是相等的,则移动元素的期望值为:

image-20250628103004979

合并有序顺序表

La, Lb中数据元素按值非递减有序排列。

void MergeList_Sq(SqList La, SqList Lb, SqList &Lc){
	pa = La.elem;
    pb = Lb.elem;
    Lc.listsize = Lc.length = La.length + Lb.length;
    pc = Lc.elem = (ElemType*)malloc(Lc.listsize*sizeof(ElemType*sizeof(ElemType));
	if(!Lc.elem)	exit(OVERFLOW);
	pa_last = La.elem + La.length - 1;
	pb_last = Lb.elem + Lb.length - 1;
    while(pa<=pa_last && pb<=lb-last){
        if(*pa <= *pb){
            *pc++ = *pa++;
        }
        else{
            *pc++ = *pb++;
        }
    }
	while(pa <= pa_last)	*pc++ = *pa++;
	while(pb <= pb_last)	*pc++ = *pb++ 
}

掌握线性表的链式表示和实现

线性链表:用一组地址任意的存储单元存放线性表中的数据元素。

由于此链表的每个结点中只包含一个指针域,故称为线性链表或单链表

带头结点的单链表的C语言实现

Typedef struct LNode{
    ElemType data;
    Struct Lnode *next;
}LNode, *LinkList;

//创建新链表
void CreateList_L(LinkList &L, int n){
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
    for(int i=n; i>0; --i){
        p = (LinkList)malloc(sizeof(LNode));
        scanf(&p->data);	//格式化字符串漏洞...
        p->next = L->next;
        L->next = p;	//插到表头
    }
}


//取出第i个结点
Status GetElem_L(LinkList L, int i, ElemType &e){
    p = L->next;
    j = 1;
    while(p && j<i){
        p = p -> next;
        ++j;
    }
    if(!p || j>i)	return ERROR;	//第i个元素不存在,或者i=0
    e = p->data;					//取得第i个元素
    return OK;
}

//在第i个结点前插入结点e
Status ListInsert_L(LinkList L, int i, ElemType e){
    p = L;
    j = 0;				//可能在头节点前面插入
    while(p && j<i-1){
        p = p->next;
        ++j;
    }
    if(!p || j>i-1)	return ERROR;
    s = (LinkList)malloc(sizeof(LNode));
    s.data = e;
    /*关键部分*/
    s->next = p->next;
    p->next = s;
}

//删除链表中的第i个结点
Status ListDelete_L(Linklist L, int i, ElemType &e){
    p = L;
    j = 0;
    while(p->next && j<i-1){	//注意条件里是p->next和!p的区别
        p = p->next;
        ++j;
    }
    if(!(p->next) || j>i-1 )	return ERROR;
    /*关键部分*/
    q = p->next;
    p->next = q->next;
    
    e = q->data;
    free(q);
    return OK;
}

//将单链表重新置为一个空表
void ClearList(&L){
    while(L->next){
        p = L->next;
        L->next = p->next;
        free(p);
    }
}

结点的插入

插入遵循先右后左的操作

image-20250620140911764

时间复杂度:O(ListLength(L))

结点的删除

image-20250620141037405

时间复杂度:O(ListLength(L))

合并链表(这代码没啥营养理解前面就行)

void union(List &La, List &Lb){
    La_len = ListLength(La);
    Lb_len = ListLength(Lb);
    for(int i=1; i<Lb_len; i++){
		GetElem(Lb, i, e);
        if(!LocateElem(La, e, equal()))
            ListInsert(La, ++La_len, e)''
    }
}

当以链式映像实现抽象数据类型线性表时 时间复杂度为:

\(O(ListLength(La) ListLength (Lb) +ListLength^2(Lb) )\)

合并有序链表(这个有一点点营养)

void MergeList(List &La, List &Lb, List &Lc){
    pa = La->next;
    pb = Lb->next;
    Lc = pc = La;
    while(pa && pb){
        if(pa->data <= pb->data){
            pc->next = pa;
            pc = pa;
            pa = pa->next;
        }
        else{
            pc->next = pb;
            pc = pb;
            pb = pb->next
        }
        pc->next = pa? pa:pb;
        free(Lb);
    } 
}

循环链表

最后一个结点的指针域的指针又指回第一个结点的链表

image-20250628135100405

和单链表的差别仅在于,判别链表中最后一个结点的条件不再是“后继是否为空”,而是后继是否是头节点

在单链表中,从一已知结点出发,只能访问到该结点及其后续结点,无法找到该结点之前的其它结点。而在单循环链表中,从任一结点出发都可访问到表中所有结点,这一优点使某些运算在单循环链表上易于实现

双向链表

堆的Unlink

typedef struct DuLNode{
    ElemType data;
    struct DuLNode *prior;
    struct DuLNode *next;
}DuLNode, *DuLinkList;

克服单向性缺点,只能往后找,往前找要从头开始。

image-20250628135500483

双向链表的插入

image-20250628135556608
s->next = p->next;
p->next = s;
s->next->prior = s;
s->prior = p;

一共需要修改四个指针,依旧按照先改前,后改后;先动extra结点,后动现有结点的原则

双向链表的删除

image-20250628140424179
p->prior->next = p->next;
p->next->prior = p->prior;

改两个指针就OK,依旧先前后后/.

静态单链表

有时也可以借用一维数组来描述线性链表,其类型说明如下

静态链表主要用于不设“指针”类型的高级程序设计语言中使用链表结构出

静态链表的特点

存储方式:

静态单链表将所有节点存储在一个连续的数组中,每个节点通常包含数据元素和指向下一个节点的游标(类似于指针,但存储的是数组下标)

内存分配:

静态单链表的内存是预先分配好的,不需要动态内存分配

访问效率:

由于数据存储在连续的数组中,静态单链表在访问元素时,可以像数组一样直接通过索引访问,提高了访问效率

插入/删除操作:

插入和删除节点时,不需要移动大量元素,只需修改节点的游标,操作相对简单

空间利用率:

静态单链表的空间利用率不如动态链表,因为需要预先分配固定大小的数组空间

静态链表的C语言描述

PPT P73

#define MAXSIZE 1000
typedef struct{
    ElemType data;
    int cur;
}component, SlinkList[MAXSIZE];


void InitSpace_SL(SLinkList &space){
    for(int i=0; i<MAXSIZE-1; i++){
        space[i].cur = i+1;
    }
    space[MAXSIZE-1].cur = 0;
}

//在静态单链线性表L中查找第1个值为e的元素。若找到,则返回它在L中的位序,否则返回0。
int LocateElem_SL(SLinklist S, ElemType e){
    i = S[0].cur;
    while(i && S[i].data!=e){	//i = S[i].cur相当于p-next!= null,因为尾结点的cur == 0;
        i = S[i].cur;
    }
    return i;
}

能够从时间和空间复杂度的角度综合比较线性表两种存储结构的不同特点及其适用场合

例题中的 10,11,13,14,15


第三章 栈和队列

掌握栈和队列类型的特点,并能在相应的应用问题中正确选用它们。

定义

  • 只能在表尾进行插入或删除。通常称其为栈顶(top)
  • 后进先出的线性表(简称LIFO表)

栈的应用

回文检测(必考)

方法1:

试写一个算法,识别依次读入的一个以@为结束符的字符序列是否为形如‘序列1&序列2’模式的字符序列。其中序列1和序列2中都不含字符‘&’,且序列2是序列1的逆序列。例如,‘a+b&b+a’是属该模式的字符序列,而‘1+3&3-1’则不是。

BOOL Symmetry(char a[])
{
    int i=0;
    Stack s;
    InitStack(s);
    ElemType x;
    while(a[i]!='&' && a[i]){
        ____    
        i++;
    }
    if(!a[i]) return FALSE;
    i++;
    while(a[i]){
        Pop(s,x);
        if(x!=a[i]){
            DestroyStack(s);
            return FALSE;
        }
        i++;
    }
    return TRUE;
}
A.Pop(s,a[i++]);  B.Push(s,a[i++]);  C.Push(s,a[i]);  D.Pop(s,a[i]);

方法2:

function isPalindrome(string):
  stack = new Stack()
  n = length(string)
  mid = n / 2

  // 将字符串的前一半入栈
  for i from 0 to mid - 1:
    stack.push(string[i])

  // 如果字符串长度为奇数,则忽略中间字符
  start = (n % 2 == 0) ? mid : mid + 1

  // 比较栈顶元素和字符串后一半的字符
  for i from start to n - 1:
    if (stack.isEmpty() or stack.pop() != string[i]):
      return false

  // 如果栈为空且所有比较都成功,则是回文串
  return true

数值转换

每次取余,存到栈中,最后输出栈

括号匹配的检验

检验括号是否匹配的方法可用“期待的急迫程度”

期待的急迫程度:如果碰到右括号,那么离他最近的左括号,必须要跟他匹配,否则就是非法括号

  • 出现左括弧,进栈
  • 出现右括弧
    • 栈空,多余,非法
    • 栈不空,与栈顶元素匹配
      • 相匹配,左括弧出栈
      • 不匹配,非法
  • 检验结束
    • 栈空,合法
    • 栈不空,非法

行编辑程序问题

是退格,@是退行

不断进栈,#时,出栈;@时,清空栈

注意要单独吞掉换行符

迷宫求解

  • 若当前位置“可通”,则纳入路径,继续前进
  • 若当前位置“不可通”(墙、探索过的路、已经走过的路),则后退,换方向继续坍缩
  • 若四周“均无通路”,则将当前位置从路径中删除出去。
typedef struct{
    int ord;	//通道块在路径上的序号
    PosType seat;	//通道块在迷宫中的坐标位置
    int di;		//从此通道块走向下一个通道块的方向
}SELemType;

Status MazePath(MazeType maze, PosType start, PosType end){
    InitStack(S);
    curpos = start;	//当前位置是入口,curpos是当前位置
    curstep = 1;	//探索第一步,curstep代表是第几步
    do{
        //能通
        if(Pass(curpos)){
            //判断当前位置是否是没走过的
            FootPrint(curpos);
            e = (curstep, curpos, 1);
            Push(S,e);
            if(curpos == end)	return TRUE;
            curpos = NextPos(curpos,1);
            curstep++;
        }
        //不通
        else{
            if(!StackEmpty(S)){
                Pop(S,e);	//先弹出来
                while(e.di=4 && !StackEmpty(S)){
                    //如果四个方向都走过了,那这里不能通行,退回一步
                    MarkPrint(e.seat);
                    Pop(S,e);
                }
                if(e.di<4){
                    e.di++;	//换个方向探索
                    Push(S,e);
                    curpos = NextPos(e.seat, e.di);
                }
            }
        }
    }while(!StackEmpty(S));
    return FALSE;
}

表达式求值

后缀表达式求值

先找运算符、再找操作数

算法描述
  • 设立操作数或运算结果栈 OPND
  • 设立表达式的结束符为“#”,予设操作数栈为空栈
  • 依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则退出栈顶两个元素进行计算,直至整个表达式求值完毕(即当前读入的字符为‘#’)
算法
OperandType EvaluateExpression(){
    initStack(OPND);
    c = getchar();
    while(c!='#'){
        if(!In(C,OP)){
			Push(OPND,c);	//不是运算符则进栈
            c = getchar();
        }
        else{
            Pop(OPND,b);
            Pop(OPND,a);
            Push(OPND, Operate(a,c,b));
        }
    }
    return GetTop(OPND);
}
如何求得后缀表达式
image-20250618183726191

每个运算符得运算次序要由它之后得一个运算符来决定

优先度高的运算符领先于优先度低的运算符

算法描述
  • 设立操作符栈
  • 设表达式的结束符为“#”,予设运算符栈的栈底为‘#’
  • 若当前字符是操作数,则直接发送给后缀式
  • 若当前字符是运算符
    • 若运算符优先数高于栈顶运算符,则进栈
    • 若运算符优先级低于栈顶运算符,则将栈顶运算符发送给后缀式
  • “(”对它之前后的运算符起隔离作用,“)”可视作为自相应左括弧开始的表达式的结束符。也就是说,“(”无条件进栈(优先级最高),直到遇见“)”(优先级最低)3
中缀表达式求值
  • 设立操作符栈OPTR,操作数或运算结果栈OPND
  • 设表达式的结束符为“#”,予设运算符栈的栈底为“#”,操作数栈为空栈
  • 依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符都是“#”)

熟练掌握栈类型的两种实现方法,特别应注意栈满和栈空的条件以及它们的描述方法。

顺序栈

#define STACK_INIT_SIZE 100;	//存储空间初始发配量
#define STACKINCREAMENT 10;		//存储空间分配增量
typedef struct{
    SElemType *base;	//栈底指针
    SelemType *top;		//栈顶指针,指向的是栈顶的下一个位置
    int stacksize;		//当前可使用的最大容量,以元素为单位
}SqStack;

[!NOTE]

栈的操作和线性表相同,只是我们限定了其插入和删除的位置。Top不是指向栈顶,而是栈顶的下一个位置,表示要插入的位置。否则我们没有办法判空

//初始化栈
Status InitStack(SqStack &S){
    S.base = (SElemType*)malloc(STACK_INIT_SIZE*sizeof(ElemType));
    if(!S.base)	exit(OVERFLOW);
    S.top = S.base;
    S.stacksize = STACK_INIT_SIZE;
}

//取得栈顶元素。一定不能少了初始条件判定,这里只返回,不弹出
Status GetTop(SqStack S, SelemType &e){
    if(S.top == S.base)	return ERROR;
    e = *(S.top-1);
    return oK;
}

//压栈
Status Push(SqStack &S, SelemType e){
    //首先判断栈是否有空间
    if(S.top - S.base >= S.stacksize){
        S.base = (ElemType *)realloc(S.base, (S.stacksize+STACKINCREAMENT)*sizeof(ElemType));
        if(!S.base) exit(OVERFLOW);
        // 新的top位置等于栈底地址+栈大小
        S.top = S.base + S.stacksize;
        S.stacksize += STACKINCREAMENT;
    }
    //先插入进去,然后top自增
    *S.top++ = e;
    //S.top++;
    return OK;
}

// 出栈
Status Pop(SqStack &S, SElemType e){
    if(S.top==S.base)	return ERROR;
    // 先自减再返回
    //S.top--;
    e = *--S.top;
    return OK;
}

[!IMPORTANT]

要注意的是,top指针指向最后一个元素的上方。

push的时候先加入元素然后指针上移动

pop的时候指针先下移,然后再取元素

链式栈

链栈的C语言实现

由于单链表有头指针,而栈顶指针也是必须的,那干吗不让它俩合二为一呢,所以比较好的办法是把栈顶放在链栈的头部(如下图所示)。另外,都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的。

img

如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。

链栈的PUSH

img
s->next = stack->top;
stack->top = s;

链栈的POP

img
p = stack->top
stack->top = stack->top->next;
free(p);

熟练掌握循环队列和链队列的基本操作实现算法,特别注意队满和队空的描述方法。

队列

  • 先进先出
  • 允许插入的是队尾(rear),允许删除的是队头(front)

链队列

队列不太适合用顺序表示,因为如果在队首删除元素,需要频繁移动元素

其中对于链列表来说,front是头节点,不是队列中的第一个结点

image-20250618185730938

//结点类型
typedef struct QNode{
    QElemType data;
    struct QNode* next;
}QNode, *QueuePtr;

//链队列类型
typedef struct{
    QueuePtr front;
    QueuePtr rear;
}LinkQueue;

//初始化
Status InitQueue(LinkQueue &Q){
    Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
    if(!Q.front)	exit(OVERFLOW);
    Q.front->next = NULL;
    return OK;
}

//销毁队列。从头节点开始一个一个释放
Status DestroyQueue(LinkQueue &Q){
    while(Q.front){
        Q.rear = Q.front->next;
        free(Q.front);
        Q.front = Q.rear;
    }
    return OK;
}

//入列
Status EnQueue(LinkQueue &Q, QElemType e){
    //插入e为新的队尾元素
    p = (QueuePtr)malloc(sizeof(QNode));
    p->data = e;
    p->next = NULL;
    Q.rear->next = p;
    //rear = rear->next;
    Q.rear = p;
}

//出列
Status DeQueue(LinkQueue &Q, QElemType &e){
    if(Q.front = Q.rear)	return ERROR;
    p = Q.front->next;
    e = p->data;
    Q.front->next = p->next;	//这句很重要
    //记得要判断下是不是没元素了
    if(Q.rear == p){
        Q.rear = Q.front;
    }
    free(p);
    return OK;
}

循环队列

注意,这里头指针front指向第一个元素(队头),而尾指针rear指向队尾的下一个元素

队列满的条件:队尾+1(mod n)等于队首

#define MAXQSIZE 100

typedef struct{
    QElemType *base;	//动态分配存储空间
    int front;	//头指针,若队列不空指向队列头元素
    int rear;	//尾指针,若队列不空指向队列尾元素的下一个位置
}SqQueue;

Status InitQueue(SqQueue &Q){
    Q.base = (ElemType*)malloc(MAXQSIZE*sizeof(ElemType));
    if(!Q.base)	exit(OVERFLOW);
    Q.front = Q,rear = 0;
    return OK;
}

Status EnQueue(SqQueue &Q. ElemType &e){
    if((Q.rear+1)%MAXQSIZE == Q.front){
        return ERROR;
    }
    Q.base[Q.rear] = e;
    Q.rear = (Q,rear+1)% MAXQSIZE;
    return OK;
}

status DeQueue(SqQueue &Q, ElemType &e){
    if(Q.front == Q.rear)	return ERROR;
    e = Q.base[Q.front];
    Q.front = (Q.front+1)%MAXQSIZE;
    return OK;
}

理解递归算法执行过程中栈的状态变化过程。

函数调用过程


第四章 串

理解“串”类型定义中各基本操作的特点,并能正确利用它们进行串的其它操作。

定义

串是由零个或多个字符组成的有限序列。空串也是串

  • 长度:串中字符的数目
  • 空串:含零个字符的串
  • 空格串:由一个或多个空格组成的串
  • 子串:串中任意个连续的字符组成的子序列
  • 字符在串的位置:字符在序列中的序号
  • 子串在串中的位置:子串的第一个字符在串中的位置(从1开始)
  • 相等:当且仅当两个串的值相等

操作集

  • Index(S,T,n):T在主串S第n个字符之后第一次出现的位置

  • Replace(&S,T,V):用V替换主串S中出现的所欲和T相等的串

  • StrInsert(&s,pos,T):S中插入T,T的第一个字符的起始位置是pos

  • Substring(&Sub,S,pos,len):从S的第pos个字符开始,长度是len

我们可以用串比较、求串长、求子串来实现查找函数Index

理解串类型的各种存储表示方法。

定长顺序存储表示

  • 用一组地址连续的存储单元存储串值的字符序列。
  • 可用定长数组描述。所以长度超过上界的时候只能截尾
  • 串长有两种表示方法
    SString[0]表示;
    串值后面加一个不计入串长的结束标记“\0”

堆分配存储表示

malloc free

chunk这一块......

块链存储表示


第五章 数组与广义表

1.了解数组的两种存储表示方法,并掌握数组在以行为主的存储结构中的地址计算方法。

两种顺序映像的方式

行序为主序

image-20250628155520163

列序为主序

以行为主的地址计算方法

image-20250618133957284

2.掌握对特殊矩阵进行压缩存储时的下标变换公式

特殊矩阵的压缩存储

以下我们都讨论行优先

对称矩阵

可以将n2个元素压缩存储到\({(1+n)n}/{2}\)中的一维空间中

具体的对应规则为:

若 i >= j

\[k = \frac{i(i-1)}{2}+j-1 \]

若 i<j

\[由对称矩阵性质得:a_{ij} = a_{ji} ,所以ij互换即可\\ k = \frac{j(j-1)}{2}+i-1 \]

三角矩阵

如果是存储下三角矩阵,那么和对称矩阵(i>=j)的情况一样

[!TIP]

下三⾓矩阵:当⼀个⽅阵的主⾓线以上的所有元素皆为零;
\(i > = j\)时,\(k = i * ( i + 1 ) / 2 + j - 2\);

如果是存储上三角矩阵

\[\begin{aligned}k = \frac{(n+(n-i+1))i}{2}+j-1 \\=\frac{i(2n-i+1)}{2}+j-i\end{aligned} \]

三对角矩阵

k = 2i + j

3.了解稀疏矩阵的两类压缩存储方法的特点和适用范围,领会以三元组表示稀疏矩阵时进行矩阵运算采用的处理方法。

稀疏矩阵

假设m行n列的矩阵含有t个非零元素,则称

\[\sigma = \frac{t}{m \times n} \]

为稀疏因子,通常认为σ <=0.05的矩阵为稀疏矩阵.

也就是说,非零元素小于总元素的5%,就是稀疏矩阵

两类稀疏矩阵

  • 特殊矩阵

    非零元在矩阵中的分布有一定规律

    • 三角矩阵,对角矩阵
  • 随机稀疏矩阵

随机稀疏矩阵的压缩存储方法

三元组顺序表
#define MAXSIZE 12500
typedef struct{
	int i,j;	//非零元的行和列
    ElemType e;	
}Triple;
typedef struct{
    Triple data[MAXSIZE+1]:	//非零三元组表, data[0]未用.
	int mu,nu,tu;	//矩阵的行数、列数、非零元个数
}TSMatrix;
求转置

Num数组代表每一列的非零元数目

Cpot数组代表每一列第一个非零元的位置

image-20250628164320723
Status FastTransposeSMatrix(TSMatrix M, TSMatrix &T){
    T.mu = M.mn;
    T.nu = M,mu;
    T,tu = M.tu;
    if(T.tu){
        
        //初始化辅助数组
        for(int col=1; col<=M.nu; col++){
            num[col] = 0;
        }
        for(int t=1; t<=M.tu; t++){
            num[M.data[t].j]++;	//统计每列元素个数
        }
        cpot[1] = 1;	//初始化
        for(col=2; col<=M.nu; col++){
            cpot[col] = cpot[col-1] + num[col-1];
        }
        
        //转置
        for(int p=1; p<=M.tu; p++){
            Col = M.data[p].j;	//先得到这个非零元素的列
            q = cpot[col];		//取得转置后的位置
            T.data[q].i = M.data[p].j;
            T.data[q].j = M.data[p].i;
            T.data[q].e = M.data[p].e;
            cpot[col]++;
        }
    }
    return OK;
}

时间复杂度为: O(M.nu+M.tu)

做乘法

行逻辑连接的顺序表
十字链表

4.掌握广义表的结构特点及其存储表示方法

定义

  • 广义表是线性表的推广,也称为列表
  • 列表中的数据元素可以为原子或列表
  • 采用链式存储结构,每个数据元素用结点表示
  • 广义表是递归定义的

结构特点

  • 广义表中的数据元素有相对次序

  • 广义表的长度定义为最外层包含元素个数

  • 广义表的深度定义为所含括弧的充数

  • 广义表可以共享

  • 广义表是一个递归的表

    递归表的深度是无穷值、长度是有限值

  • 任何一个非空广义表均可分解为表头和表尾。

表头是原子或列表,表尾一定是列表

例子

image-20250618135726151

找到最左边的左括号,然后向右移动,找到合法的位置。过程中没有消除括号。(这也是表头表尾分析法的步骤)

广义表的存储结构

image-20250618135945070
typedef enum{
    ATOM,
    LIST
}ElemTag;

typedef struct GLNode{
    ElemTag tag;
    union{
        AtomType atom;
        struct{struct GLNode *hp, *tp;} ptr;
    };
}*Glist;

表头表尾分析法

image-20250618140337822 image-20250618140408578

子表分析法

提取元素,每次要去掉最外层的括号

image-20250618140901983

5.学习利用分治法的算法设计思想编制递归算法的方法

相关算法(类比一下二叉树)

求广义表深度

归纳项

广义表的深度 = Max{子表的深度} + 1

基本项

空表的深度 = 1

原子的深度 = 0

int GlistDepth(Glist L){
	if(!L)	ruturn 1;
    if(L->Tag = ATOM) return 0;	
    for(max=0,pp=L; pp; pp=pp->ptr.tp){;
        dep = GlistDepth(pp->ptr.hp);
        if(dep > max)	max = dep;
    }
    return max+1
}

dep = GlistDepth(pp->ptr.hp);
if(dep > max) max = dep;

image-20250629083357129

复制广义表

归纳项

复制新的表头 + 新的表尾

基本项

空表

原子结点

Status CopyGList(Glist &T, Glist L){
	if(!L)	T = NULL;
    else{
        if(!T = (Glist)malloc(sizof(GLNode)))	exit(OVERFLOW);
        T->tag = L->tag;
        if(L->tag = ATOM){
            T->atom = L->atom;
        }else{
            CopyGList(T->ptr.hp, L->ptr.hp);
            CopyGList(T->ptr.tp, L->ptr.tp);m
        }
        return OK;
    }
}

第六章 树和二叉树

1.掌握森林-树-二叉树-完全二叉树-满二叉树的定义术语

满二叉树:深度为k且含有2k-1个结点的二叉树。简单来说,每一层达到最大

完全二叉树:树种所含的n个结点和满二叉树中编号为 1 至 n的结点一一对应。简单来说吗,达到了n层,那么前n-1层全满,第n层从左向右排列

树型结构和线性结构的结构特点

  • 树和线性结构,第一个元素(根)都无前驱,最后一个元素(叶子结点)都无后继。
  • 对于其他元素,线性结构中有一个前驱,一个后继;树型结构有一个前驱,多个后继。

基本术语

结点:数据元素+若干指向子树的分支

结点的度:结点拥有的子树的数目

树的度:树中所有结点的度的最大值

叶子结点:度为零的结点

分支结点:度不为零的结点

结点的层次:假设根结点的层次为1,根的孩子为第二层。第l 层的结点的子树根结点的层次为l+1

树的深度(高度):树种叶子结点所在的最大层次

定义

森林:是m(>=0)棵互不相交的树的集合。任何一棵非空树是一个二元组Tree = (root, F)。其中root是根节点,F被称为子树森林

二叉树:二叉树或为空树,或是由一个根结点加上两棵分别称为左子树和右子树的、互不交的二叉树组成

2.熟练掌握二叉树的结构特性,了解相应的证明方法

五种基本形态

空树、只有根、只有左子树、只有右子树、左右子树都不空

二叉树的五个性质

性质1(层节点数)

\[在二叉树的第 i 层上至多有2^{i-1}个结点(i >=1) \]

用归纳法证明

性质2(总结点数)

\[深度为k的二叉树上至多含2^k-1个结点 \]

用性质1累加即可

性质3(叶子结点数)

\[对任何一棵二叉树,若它含有n_0个叶子结点、n_2个度为2的结点, \\ 则必存在关系式:n_0 = n_2+1 \]

证明

\[\begin{aligned} \because \text{是二叉树} \\ \therefore n\,\,=\,\,n_0+n_1+n_2 \\ \text{边总数} b=n_1+2n_1\,\, \\ \because \text{树的性质,} b=n-1=n_0+n_1+n_2-1 \\ \text{由此}\ \text{得} \\ n_0=n_2+1 \end{aligned} \]

性质4(完全二叉树深度)

\[具有n个结点得完全二叉树得深度为 \lfloor log_2 n \rfloor +1 \]

性质5(完全二叉树父子结点编号关系)

若对含n个结点的完全二叉树从上到下且从左到右进行1到n的编号,则任何一个结点的编号为:

  • 若 i=1,则该结点是二叉树的根,无双亲,否则,编号为 i/2 的下界的结点为其双亲结点;

  • 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;

  • 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。

3.熟悉二叉树的各种存储结构的特点及适用范围

二叉树的顺序存储表示

按照性质5结点编号,存到线性表中

二叉树的链式存储表示

二叉链表(最常用)

image-20250616191300202
typedef struct BiTNode{
	TelemType data;
	struct BiTNode *lchild, *rchild
}BiTNode,*BiTree;

三叉链表

image-20250616191410297 image-20250616191430534

区别在于多了一个parent,可以倒着往回查,二叉链表没法回退

双亲链表

image-20250616191630909 image-20250616191648551

记录父亲结点的地址,并保存他自己是左孩子还是右孩子

线索链表

二叉树的字符串表示

image-20250616203506644

4.掌握遍历二叉树的各种遍历策略的递归算法

先上后下的按层次遍历

先左后右的遍历

先根遍历

递归写法
Status PreOrderTraverse(BiTree T, Status(* Visit)(TelemType e)){
	if(T){
        if(Visit(T->data)){
            if(PreOrderTraverse(T->lchild.Visit)){
                if(PreOrderTraverse(T->rchild.Visit)){
                    return OK;
                }
            }
        }
        else return ERROR;
    }
    else return OK
}
非递归写法

TODO


中根遍历

后根遍历

遍历算法的应用举例

统计二叉树叶子结点的个数(先序遍历)

统计一下丁克就好

void CountLeaf(BiTree T, int& count){
	if(T){
        if((!T->lchild) && (!T->rchild)){
            count++;
        }
        CountLeaf(T->lchild, count);
        CountLeaf(T->rchild, count);
    }
}

求二叉树的深度(后序遍历)

二叉树的深度应为左右子树深度的最大值加一

int Depth(BiTree T){
	if(!T) depthval =0;
    else{
        depthLeft = Depth(T->lchild);
        depthRight = Depth(T-rchild);
        depthval = 1 + max(depthLeft,depthRight);
    }
    return depthval;
}

复制二叉树(后序遍历)

是一个从下往上的过程

image-20250616203139025 image-20250616203226394

二叉树转广义表

二叉树转广义表

5.理解二叉树线索化的实质,熟练掌握二叉树线索化过程

线索:指向该线性序列中的“前驱”和“后继”的指针。左前右后

一棵二叉树有 n+1 个空闲指针

把遍历序列写出来,然后左前右后画箭头就可以了。可能会有一个头节点,只需要开头的前驱和结尾的后继指向头节点即可。

6.熟悉树的各种存储结构及其特点,掌握树和森林与二叉树的转换方法

树的存储结构

双亲表示法

image-20250629085839676

孩子链表表示法

image-20250629085901308

有点像邻接表

树的二叉链表存储表示法

左孩子右兄弟

image-20250629090049686

森林与二叉树的对应关系

image-20250629090341996

7.学会编写实现树的各种操作的算法

树的遍历

树没有中根遍历

先根遍历

若树不空,先访问根节点,然后依次先根遍历各棵子树

树的先根遍历等于对应二叉树的先序遍历

后根遍历

若树不空,则先依次后根遍历各棵子树,然后访问根结点。

树的后根遍历等于对应二叉树的中序遍历

按层次遍历

若树不空,则自上而下自左至右访问树中每个结点。

森林的遍历

先序遍历森林

  1. 访问森林中第一棵树的根节点
  2. 先序遍历森林中第一棵树的子树森林
  3. 先序遍历森林中其余树构成的森林

中序遍历森林

  1. 中序遍历森林中第一颗树的子树森林
  2. 访问森林中第一棵树的根节点
  3. 中序遍历森林中第一棵树的根节点

总结

image-20250629090752404

树没有中根遍历的概念、而森林没有后序遍历的概念。

因为树变成二叉树后,根节点没有右子树,也就是说中序遍历一棵树是不可能的因为所有子节点都在左子树上。

树没有后序遍历,是因为你没有办法先访问所有的子树再访问根。

树的遍历的应用

8.了解最优二叉树和哈夫曼编码

最优二叉树

最优二叉树:带权路径长度(WPL)最短的二叉树

哈夫曼编码

是一种最优前缀码


第七章 图

image-20250611140814075

图的定义和术语

图的定义

结构定义:由一个顶点集 V 和一个 弧集 R 构成的数据结构

有向图、无向图

弧是有方向的,顶点集和弧集构成的图是有向图

顶点集和边集构成的图称作无向图

网、子图

弧或边带权的图分别称为有向网和无向网

从原图中扣一坨下来,就是子图

完全图、稀疏图、稠密图

n个顶点,e条边

e = n(n-1)/2 条边的无向图是 完全图

e = n(n-1) 条弧的有向图是 有向完全图

若 e < nlogn,则称为稀疏图

若e >= nlogn,则称为稠密图

v和w之间存在一条边,则称其为邻接点

一个顶点的连接的边数就是度数

image-20250611143809730

有向图中,顶点的出度:以顶点v为弧尾的弧的数目;顶点的入读:以顶点v为弧头的弧的数目。度=入读+出度

路径、回路

路径上边的数目叫做路径长度

简单路径:序列中顶点不重复

简单回路:除了首尾顶点相同,其余顶点不重复出现

连通图、强连通图

连通图:任意两个顶点之间都有路径相通

连通分量:若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量。

强连通图:对于有向图,若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。否则称其各个强连通子图为强连通分量。

生成树、生成森林

对于连通图,选n-1个边构成的极小连通子图就是生成树

对于非连通图,各个连通分量构成的生成树就是生成森林。

图的存储结构

邻接矩阵

无向图的邻接矩阵一定是对称矩阵,而有向图的邻接矩阵不一定是对称矩阵

邻接表

无向图的邻接表

  • 先按顺序给结点编号(0~n)
  • 然后给相邻结点的编号加到链表中

image-20250611144924910

有向图的邻接表

  • 只找出度,不找入读
image-20250611145019992

可见,再有向图的邻接表中不易找到指向该顶点的弧

在无向图中,找到指向该顶点的弧呢?(找往外指的边即可)

有向图的逆邻接表

  • 只找入度,不找出度
image-20250611145243804

在有向图的逆邻接表中,对每个顶点,链接的是指向该顶点的弧。

十字链表

邻接多重表

图的遍历

深度优先搜索

从图中某个顶点V0 出发,访问此顶点,然后依次从V0的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和V0有路径相通的顶点都被访问到

若此时图中尚有顶点未被访问(非连通图),则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直到图中所有顶点都被访问到为止。

例子

image-20250611150529211

从图解可以看出

  • 深度优先搜索遍历连通图的过程类似于树的先根遍历
  • 我们可以设置一个访问标志 visited[w], 初值为false,被访问后为true

针对连通图,我们可以编写以下dfs算法

void DFS(Graph G, int v){
	// 从顶点v出发,深度优先遍历连通图G
    visited[v] = TRUE;
    VisitFunc(v);
    for(w=FirstAdjVex(G,v); w!=0; w=NextAdjVex(G,v,w))
        if(!visit[w])
            DFS(G,w);
}

一般图的dfs

void DFSTraverse(Graph G, Status(*Visit)(int v)){
    //对一般图G作深度优先遍历
    VisitFunc = Visit;
    for(v=0; v<G.vexnum; ++v){
        visited[v] = FALSE;//访问标志数组初始化
    }
    for(v=0; v<G.vexnum; ++v){
        if(!visited[v])	DFS(G.v);	//对尚未访问的顶点调用DFS
    }
}

广度优先搜索

从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。

若此时图中尚有顶点未被访问(非连通图),则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

例子

image-20250611152210911

我们依次找到距离起始点距离为1、2、3……的点。

这个类似 按层次遍历。

算法

void BFSTraverse(Graph G, Status(*Visit)(int v)){
	for(v=0; v<G.vexnum; ++v)
        visited[v] = FALSE;		//初始化访问标志
    InitQueue(Q);	//置空的辅助队列Q
    for(v=0; v<G.vexnum; ++v){
        if(!visited[v]){	//如果尚未访问
            //BFS核心
            visited[v] = TRUE;	//标记
            Visit(v);
            EnQueue(Q.v);	//入队列
            while(!QueueEmpty(Q)){
                DeQueue(Q,u);	//队头出队列并置为u
                for(w=FirstAdjVex(G,u); w>=0; w=NextAdjiVex(G,u,w)){
                    if(!visited[w]){
                        visited[w] = true; 
                        Visit[w];
                    	Enqueue(Q,w);
                    }
                }
            }
        }
    }
}

图的连通性问题

无向图的连通分量和生成树

对无向连通图进行遍历,DFS得到深度优先生成树、BFS得到广度优先生成树

TODO

最小生成树

普利姆算法(必考)

image-20250615195123454

每次找到最下代价点后,加入生成树中,更新邻接点和最小代价

详见ppt p72

时间复杂度

O(n^2) 适用于稠密图(e >= nlogn)

算法
typedef struct{
    VertexType adjvex;
    VRType lowcost;
}closedge[MAX_VERTEX_NUM];


void MiniSpanTree_PRIM(MGraph G, VertexType u){
    // 准备工作:定位、初始化closedge
    k = Locatevex(G,u);
    for(int i=0; i<G.vexnum; i++){
        if(i!=k)	closedge[i] = {u, G.arcs[k][i].adj};
    }
    closedge[k].lowcost = 0;
    
    // 
    for(int i=1 ;i<G.vexnum; i++){	//从第二个结点开始
        k = minimum(closedge);		//找到最小代价结点
        closedge[k].lowcost=0;		//新结点加入树中
        for(int j=0; j<G.vexnum; j++){
            if(G.arcs[k][j].adj < closedge[j].loscost){		//更新closedge
                closedge[j] = {k, G.arcs[k][j].adj};
            }
        }
    }
}

克鲁斯卡尔算法

每次找最小代价边,但不能构成回路

时间复杂度

O(eloge) 适用于稀疏图

算法
构造非连通图 ST=( V,{ } );
 k = i = 0;    // k 记录选中的边数
 while (k<n-1) {
   ++i;
  检查边集 E 中第 i 条权值最小的边(u,v);  若(u,v)加入ST后不使ST中产生回路,
  则  输出边(u,v);  且  k++;
}

有向无环图及其应用

拓扑排序

  1. 找到一个没有前驱的顶点,输出它
  2. 删去此顶点和以它为尾的弧
  3. 重复前两部直到图为空。如果图不空但找不到无前驱的顶点,那么说明有向图中存在环
image-20250629094430016

求关键路径

image-20250629095806129 image-20250629100349076 image-20250629100616117
关键路径
  • 设计中从输入到输出经过的延时最长的逻辑路径
关键活动
  • 关键路径上的活动
  • 该弧上的权值增加将使有向图上的最长路径的长度增加
事件(顶点)最早发生时间
  • 源点到顶点VJ的最长路径长度
  • 决定了所有以vj为尾的弧所表示的活动最早发生时间
  • 求ve的顺序按照拓扑有序的次序
事件(顶点)最晚发生时间
  • 表示再不推迟整个工程完成的前提下,事件最迟发生的时间

  • 工程完成时间 - 从顶点Vk到汇点的最长路径长度

  • 求vl的顺序按照拓扑逆序的次序

活动最早发生时间
  • 找弧尾事件的最早发生时间
活动最晚发生时间
  • 弧头活动的最晚发生时间 - 活动持续时间

最短路径

单源最短路径

Dijkstra算法(必考)

image-20250615202055688

求从源点到其余各点的最短路径的算法

void ShortestPath_DIJ(MGraph G, int v0, PathMatrix &p, ShortPathTable &D){
    //使用迪杰斯特拉算法求有向网G的v0顶点到其余顶点v的最短路径P[v]及其带权长度D[v]
    //若P[v][w]为True,则w是从v0到v当前求得最短路径上的点
    //final[v]为true当且仅当已经求得v0到v得最短路径,即v属于S集
    
    //初始化准备
    for(v=0; v<G.vexnum; v++){
        final[v]=false; 	//S集合设空
        D[v] = G.arcs[v0][v];	//初始化代价
        for(int w=0 ;w<G.vexnum; w++){	//设空路径
            p[v][w] = false;
        }
        if(D[v]<INFINITY){	//邻接点加入路径
            p[v][v0] = true;
            p[v][v] = true;	//对角线都是T,每个点的最短路径包含自己
        }
    }
    D[v0] = 0;
    final[v0] = true;	//v0作为起始点加入S集
    
    //开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集
    for(int i=1; i<G.vexnum; i++){
        min = INFINITY;
        //1.找到最小代价D[w]结点v
        for(w=0; w<G.vexnum; w++){
            if(!final[w] && D[w]<min){	//不在S集且有更短路径就更新v和最小距离
                v = w;
                min = D[w];
            }
        }
        //2.S集说脑婆恰个v
        final[v] = true;
        //3.更新最小代价和路径
        for(w=0; w<G.vexnum; w++){
            if(!final[w] && (min+G.arcs[v][w]<D[w]) ){
                D[w] = min+G.arcs[v][w];
                p[w] = p[v];	//复制路径
                p[w][w] = true;	//加入结点w
            }
        }
    }
}

多源最短路径

floyd算法

image-20250629102524747

image-20250629102535803


第九章 查找

image-20250609202119488

1.顺序表和有序表的查找方法,掌握折半查找算法的实现。

静态查找表:仅供查询检索

动态查找表:查询之后,如果结果为不在表中,可以插入;在表中,可以删除

顺序表

查找方法

用数组或链表存储的无序表,或者是用链表存储的有序表

如果数据是顺序表而不是有序表,我们采用带哨兵的数组。

image-20250609210454954

0号单元为监视哨,从后往前寻找,我们会省略很多步骤,

  1. 不用判断i是否超界限,因为最终总会停
  2. 每次循环的时候,少一个条件判断,最后少一步条件判断。不用判断是否等于0,直接return就行。如果为0,说明没有找到。
int Search_Seq(SSTable ST. KeyType key){
	ST.elem[0] = key;		//这步很关键
	for(int i=ST.length; ST.elem[i]!=key; i--){
		reuturn i;
	}
}

有序表

折半查找

有序表表示静态查找表,并且是以顺序结构存储。那我们我们可以使用折半查找

代码如下

int Search_Bi(SSTable ST, KeyType key){
	low = 1; high = ST.length;	//置区间初值
    while(low <= high){
		mid = (low+high)/2;
        if(EQ(key, ST.elem[mid].key))
            return mid;
        else if(LT(key, ST.elem[mid].key))
            high = mid - 1;		//前半区
        else
            low = mid + 1;		//后半区
    }
    reuturn 0;
}

时间空间复杂度

image-20250609213358646

从查找性能看,最好情况能达到O(logn),此时要求表有序

从插入和删除的性能看,最好情况能达到O(1),此时要求存储结构是链表

2.熟练掌握二叉排序树、平衡二叉树的构造和查找方法。

二叉排序树

定义

或者是一棵空树;或者任意一个节点,它的左子树(若不空)所有节点小于它,右子树(若不空)所有节点大于它在(左小右大)。

typedef struct BiTNode{
	TElemType data;
	struct BitNode *lchild, *rchild;	//左右孩子
}BitNode, *BiTree;

查找算法

等于则成功,小于则向左,大于则向右,直到遇见空。

所以这里用到了递归的思想

BiTree SearchBST(BiTree T, KeyType key, BiTree f, BiTree &p){
    if(!T){
        p = f;				//都为NULL
        return FALSE;		//空树查找失败
    } 
    else if(EQ(key, T->data.key)){			//退出条件	
        p = T;
        return TRUE;
    }
    else if(LT(key, T->data.key)){
        SearchBST(T->lchild, key, T, p);
    }
    else SearchBST(T->rchild, key, T, p);
}

如果查找成功p指向第四个参数,否则p指向查找路径上最后一个点,第三个参数f永远指向双亲,初始值为null

插入算法

插入必须在查找不成功的时候进行

插入的一定是叶子节点

Status InsertBST(BiTree &T, ElemType e){
    if(!SearchBST(T, e.key, NULL, p)){
        s = (BiTree)malloc(sizeof(BiTNode));
        s->data = e; s->lchild=s->rchild-NULL;
        if(!p) T=s;	//被插节点是新的根节点
        else if(LT(e.key, p->data.key)){
            p->lchild = s;
        }
        else p->rchild = s;
    }
    return TRUE;
}

删除算法

删除在查找成功之后进行,并且要求在删除二叉排序树上某个结点之后,仍然保持二叉排序树的特性。

分为三种情况,如提纲图

  • 删除的是叶子节点

​ 双亲结点设置相应指针域为空

  • 删除的结点只有左子树或右子树

    双亲结点的相应指针域的值改为 “指向被删除结点的左子树或右子树”

    • 右子树为空只需要重接它的左子树
      image-20250609222705351
    • 左子树为空只需重接它的右子树

image-20250609223005133

  • 删除的结点既有左子树,也有右子树

​ 以其前驱左子树的最右边)替代之,然后再删除该前驱结点。(其前驱结点一 定是1、2种情况)

Status DeleteBST(BiTree &T, KeyType key){
    if(!T) return FALSE;
    else{
        if(EQ(key,T->data.key)){
			return Delete(T);
        }
        else if(LT(key, T->data.key)){
			return DeleteBST(T->lchild, key);
        }
        else 
            return DeleteBST(T->rchild, key);
    }
}
Status Delete(BiTree &p){
    if(!p->rchild){		//右子树为空
		q = p;
        p = p->lchild;
        free(q);
    }
    if(!p->lchild){
		q = p;
        p = p->rchild;
        free(q);
    }
    else{
        q = p;
        s = p->lchild;
        while(s->rchild){
            q = s;
            s = s->rchild;
        }
        p->data = s->data;
        if(q!=p) q->rchild = s->lchild;
        else q-lchild = s->lchild;
        delete s;
    }
    return TRUE;
}

平衡二叉树

定义

又称为AVL树,是二叉排序树的一个子集。它或者是一棵空树,或者是具有以下性质二叉树

  • 它的左子树或右子树都是平衡二叉树
  • 左右子树的深度之差不超过1
结点的平衡因子

该结点的左子树高度减去它的右子树高度。即:

\[h_L -h_R \]

构建方法

插入过程中,采用平衡旋转技术

TODO

3.理解B-树、B+树的概念异同,掌握B-树插入和删除的过程。

B-树

定义

是一种平衡多路查找

性质

一棵 m 阶的B-树,或为空树,或具有以下特征

  • 所有非叶子结点均至少含有⌈m/2⌉棵子树至少有⌈m/2⌉-1个关键字),至多含有 m 棵子树
  • 节点或为叶子结点,或至少含有两棵子树
  • 所有非终端结点含有以下信息数据

​ (n,A0,K1,A1,K2,A2,…Kn,An)

  • 树种所有叶子结点均不带信息,且在树的同一层次上
image-20250609225720676

这是一个4阶B-树,有3个关键字,最少2棵子树,1个关键字。

插入

多了分裂,分裂出来的先找双亲处理,处理不掉再建新的结点

image-20250609230009366image-20250609230025973

删除

结点中关键字的个数不能小于⌈m/2⌉-1,否则,要从其左(或右)兄弟结点“借调”关键字,若其左和右兄弟结点均无关键字可借(结点中只有最少量的关键字),则必须进行结点的“合并”。

TODO

B+树

  • 每个叶子结点中含有 n 个关键字和 n 个指向记录的指针;并且,所有叶子结点彼此相链接构成一个有序链表,其头指针指向含最小关键字的结点
  • 每个非叶结点中的关键字 Ki 即为其相应指针 Ai 所指子树中关键字的最大值
  • 所有叶子结点都处在同一层次上,每个叶子结点中关键字的个数均介于⌈m/2⌉和 m 之间)(和B-树中子树的数量相同)。
image-20250609230631076

查找

有两种查找方式。从最底层顺序查找,从根节点快速检索

4.熟练掌握哈希表的构造方法,深刻理解哈希表与其它结构的表的实质性的差别。

哈希表

之前的查找表的有共同特点:记录在表中的位置和它的关键字之间不存在一个确定的关系。(最多只能缩小范围,但是无法直接确定位置,不能像数组存储二叉树一样,可以直接通过信息定位,无法进行随机查找)

ASL=0 <=> 记录在表中的位置和其关键字之间存在一种确定的关系

哈希函数

因此在一般情况下,需在关键字与记录在表中的存储位置之间建立一个函数关系,以 f(key) 作为关键字为 key 的记录在表中的位置,通常称这个函数 f(key) 为哈希函数。

  • 哈希函数是一个映象,即: 将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可;

5.掌握按定义计算各种查找方法在等概率情况下查找成功时的平均查找长度ASL。

平均查找长度ASL的计算

平均查找长度(Average Search Length)ASL,计算公式如下:

\[ASL=\sum_{i=1}^n{P_iC_i} \]

n是表长,pi是查找表中查找第i个记录的概率,且pi累加为1。Ci为找到该记录时。和给定值比较过的关键字的个数。

顺序表的ASL

对于顺序表而言

\[C_i = n-i+1 \]

\[ASL=nP_1+\left( n-1 \right) P_2++2P_{n-1}+P_n \]

在等概率查找的情况下,可得

\[ASL_{ss} =\frac{1}{n}\sum_{i=1}^n(n-i+1) = \frac{n+1}{2} \]

若查找概率无法事先预定,我们每次查找之后,将刚刚查找到的记录直接移交到表尾的位置上。

折半查找的ASL

我们构建一棵树。每次以mid值作为节点

考虑一个具体的情况

image-20250609212537252

在等概率的情况下,计算得到

\[ASL_{bs} = \frac{1}{n}\sum_{i=1}^{n}C_i=\frac{1}{n}[\sum_{j=1}^{h}j *2^{j-1}]=\frac{n+1}{n}log_2(n+1)-1 \]

其中 J 表示比较次数, 2^(j-1) 表示比较 j 次的元素个数

Log2(n)+1 是完全二叉树的深度

二叉排序树的查找分析

image-20250609224533591

等概率情况下

image-20250609224555316 image-20250609224601658 image-20250609224622809

平衡树的查找性能分析

B-树得查找性能分析

主要取决于B-树得深度

第十章 内部排序

image-20250610204353826
posted @ 2026-01-25 16:20  Pocon  阅读(3)  评论(0)    收藏  举报