预备的数学知识

等差数列求和 Sn=(a1+an)*n/2

等比数列求和 Sn=a1*(1-qⁿ)/(1-q)

数据结构的概念

基本名词

数据: 能够被计算机识别和处理的符号的集合

数据元素:是数据的基本单位,由若干的数据项构成,如一个人有手有脚

数据对象:由同类型的数据元素组成的集合,如一群人

数据类型:由一个集合和定义在此集合上的操作组成
    原子类型:值不可再分的数据类型,如int
    结构类型:值可以分解成若干成分,如struct
    抽象数据类型(ADT):数学模型和操作组成,只描述逻辑特性,不管存储结构,通常用数据对象,数据关系,基本操作集来描述它
    
数据结构:是相互之间存在特定关系的数据元素的集合,数据元素之间的关系称为结构,数据结构包括如下三方面
    逻辑结构,描述数据元素之间的逻辑关系,与数据结构无关,分为线性结构和非线性结构
    存储结构,逻辑结构在计算机中的实现(映像),主要有顺序存储、链式存储、索引存储和散列存储
    数据运算,在逻辑结构中用运算定义表示,在存储结构中用运算的实现

算法

算法的概念
    对特定问题求解步骤的一种描述,是指令的有限序列,每条指令表示一个或多个操作

算法的特性
    有穷性:有穷步,每一步在有穷时间内完成
    确定性:指令有确切的含义,不会产生二义性。相同的输入得到相同的输出
    可行性:算法能够跑得通,能够在有限次运算后结束
    输入:有零个或者多个输入
    输出:有一个或者多个输出

算法的标准
    正确性:正确解决问题
    可读性:代码易读
    健壮性:容错率高
    效率和低存储量:效率是执行时间,存储量是算法执行所要的最大存储量

算法效率的度量
    时间复杂度,取决于问题的规模n和元素的初始状态
        基本定义
            T(n) 是算法所有语句的频度和的数量级、fn(n)是基本运算的频度、O函数用来取数量级,
            则时间复杂度T(n)=O(f(n))
        时间复杂度分类
            最坏时间复杂度
            平均时间复杂度:等概率输入,算法的期望运行时间
            最好时间复杂度
        常见的时间复杂度
            O(1)<O(log₂n)<O(n)<O(nlog₂n)<O(n²)<O(n³)<O(2ⁿ)<O(n!)<O(nⁿ)
    空间复杂度,算法所耗费的存储空间
        S(n)=O(g(n))
        算法原地工作指算法所需的辅助空间是常量即O(1)

线性表

线性表的定义和基本操作

线性表的定义
    线性表是相同数据类型的n(n≥0)个数据元素的有限序列
    L=(a1,a2,...,an);
    线性表是逻辑结构,表示元素之间是相邻关系的

线性表的基本操作
    InitList(*L) 初始化表
    Length(L) 求表长
    LocateElem(L, e) 根据值e查找第一个元素的位序
    GetElem(L, i) 根据位序i,查找对应的元素
    ListInsert(*L, i, e) 在位序i上插入指定元素e
    ListDelete(*L, i, *e) 删除位序i的元素,并且用e接收删除的结点
    PrintList(L) 输出表的所有元素值
    Empty(L) 是否为空表,true或者false
    DestroyList(*L) 销毁线性表

顺序表

顺序表是存储结构,用来描述元素逻辑上相邻在物理上也相邻
元素位序表示元素是第几个,元素的下标是索引

顺序表存储结构的两种描述
    静态分配
        #define MaxSize 50
        typedef struct {
            ElemType data[MaxSize];
            int length;
        }SqList;

    动态分配
        #define InitSize 100
        typedef struct{
            ElemType *data;
            int length;
        }SeqList;
        
        SeqList L;
        L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);
        L.length=InitSize;

顺序表的特点
    随机访问,存储密度高,插入和删除效率低

顺序表增

实现在表的第3个索引位置插入元素5

算法时间复杂度分析
    最好情况:当在表尾插入,时间复杂度O(1)
    最坏情况:当在表头插入,时间复杂度O(n)
    平均情况:在第i个位置上插入是等概率的,pi=1/n,i从0~n-1,对pi*(n-i)求和,结果(n+1)/2,时间复杂度O(n)

#include <stdio.h>
#include <stdbool.h>

#define ElemType int
#define MaxSize 50

typedef struct {
	ElemType data[MaxSize];
	int Length;
}SqList;

int main(void) {
	bool AddElement(SqList *, int, ElemType);
	SqList list;
	list.data[0] = 0;
	list.data[1] = 1;
	list.data[2] = 2;
	list.data[3] = 3;
	list.data[4] = 4;
	list.Length = 5;
	
	AddElement(&list,3,5);
	return 0;
}

bool AddElement(SqList *list, int i, ElemType e) {
	if(i < 0 || i > list->Length || list->Length==MaxSize) return false;
	int count;
	for(count=list->Length-1;count>=i;--count){
		list->data[count+1] = list->data[count];
	}
	list->data[i]=e;
	list->Length++;
	return true;
}

顺序表删

删除表中第i个索引位置元素,用e存储删除的元素

算法时间复杂度分析
    最好情况:删除表尾元素,时间复杂度O(1)
    最坏情况:删除表头元素,时间复杂度O(n)
    平均情况:删除第i个结点是等概率的,pi=1/n,i从0~n-1,对pi*(n-i-1)求和,结果(n-1)/4,时间复杂度O(n)

#include <stdio.h>
#include <stdbool.h>

#define ElemType int
#define MaxSize 50

typedef struct {
	ElemType data[MaxSize];
	int Length;
}SqList;

int main(void) {
	bool DeleteElement(SqList *, int, ElemType *);
	SqList list;
	ElemType e;
	list.data[0] = 0;
	list.data[1] = 1;
	list.data[2] = 2;
	list.data[3] = 3;
	list.data[4] = 4;
	list.Length = 5;
	
	DeleteElement(&list, 3, &e);
	printf("%d\n", e);
	return 0;
}

bool DeleteElement(SqList *list, int i, ElemType *e) {
	if(i<0 || i>list->Length-1 || list->Length == 0) return false;
	
	int count = list->Length - 1;
	
	if(count == 0) {
		*e = list->data[count];
		list->Length = 0;
		return true;
	}
	
	*e = list->data[i];
	for(;count > i; --count){
		list->data[count-1] = list->data[count];
	}
	--(list->Length);
	return true;
}

顺序表查

查找第一个值等于3的元素的索引位置

算法时间复杂度分析
    最好情况:查找元素就在表头,时间复杂度为O(1)
    最坏情况:查找元素在表尾或者不存在,时间复杂度O(n)
    平均情况:要查找的第i个结点是等概率的,pi=1/n,i从0~n-1,对pi*i求和,结果(n-1)/2,时间复杂度O(n)

int FindElement(SqList list, ElemType e) {
	int i = 0;
	for(;i<list.Length;i++) {
		if(list.data[i] == e){
			return i;
		}
	}
	return -1;
}

单链表

链表也是存储结构,用来描述元素在逻辑上相邻在物理上不相邻

单链表中结点存储结构的描述
    typedef struct LNode {
        ElemType data;
        struct LNode *next;
    }LNode, *LinkList;

不带头结点的单链表,头指针为NULL时表示空表

带头结点的单链表,头指针指向此结点,数据域可以不设任何信息,也可以记录表长等信息

建立单链表

有头结点,头结点数据域存放链表长度

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

#define ElemType int

typedef struct LNode {
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

头插法建立单链表
    头插法建立的单链表的顺序和实际输入顺序相反,时间复杂度O(n)

    int main(void) {
        bool CreateLinkList(LinkList, int);
        LinkList list= (LNode*)malloc(sizeof(LNode));
        list->next = NULL;
        
        CreateLinkList(list, 100);
        
        printf("%d", list->next->data); // 99
        return 0;
    }

    bool CreateLinkList(LinkList list, int num) {
        LNode *node, *currentNode = list;
        int i = 0;
        while(i < num) {
            node = (LNode*)malloc(sizeof(LNode));
            node->data = i;
            node->next = currentNode->next;
            currentNode->next = node;
            ++i;
        }
        list->data = num;
        return true;
    }

尾插法建立单链表,时间复杂度O(n)
    bool CreateLinkList(LinkList list, int num) {
        LNode *node, *currentNode = list;
        int i = 0;
        while(i < num) {
            node = (LNode*)malloc(sizeof(LNode));
            node->data = i;
            currentNode->next = node;
            currentNode = node;
            ++i;
        }
        list->data = num;
        return true;
    }

单链表增

插入结点,将值x插入到第i个结点后面

算法时间复杂度O(n)

bool AddElement(LinkList list, ElemType e, int i) {
	LNode *currentNode = list, *node;
	int count = 0;
	if(i < 0 || i > currentNode->data) return false;
	while(count < i) {
		currentNode = currentNode->next;
		++count; 
	}
	
	node = (LNode*)malloc(sizeof(LNode));
	node->data = e;
	node->next = currentNode->next;
	currentNode->next = node;
	
    ++list->data;
    
	return true;
}

单链表删

删除结点,将第i个结点删除

时间复杂度O(n)

bool DeleteElement(LinkList list, int i) {
	LNode *currentNode = list, *preNode;
	int count = 0;
	if(i < 0 || i > currentNode->data) return false;
	while(count < i) {
		preNode = currentNode;
		currentNode = currentNode->next;
		++count; 
	}
	preNode->next = currentNode->next;
	free(currentNode);
	--list->data; 
	
	return true;
}

单链表查

按值查找结点,查找第一个数据域等于给定值e的结点

时间复杂度O(n)

    LNode* FindElement(LinkList list, ElemType e) {
        LNode *currentNode = list;
        while(currentNode != NULL) {
            if(currentNode->data == e) {
                return currentNode;
            }
            currentNode = currentNode->next;
        }
        return NULL;
    }

双链表

比单链表多了一个指向前驱结点的指针

typedef struct DNode{
	ElemType data;
	struct DNode *prior, *next;
}DNode, *DLinkList; 

双链表的操作和单链表类似,只不过在新增结点和删除结点的时候,多了处理前驱结点指针的操作

双链表插入操作

    在结点p后插入结点s
        s->next = p->next;
        p->next->prior = s;
        s->prior = p;
        p->next = s;

    在结点p前插入结点s
        s->prior = p->prior;
        p->prior->next = s;
        s->next = p;
        p->prior = s;
    
双链表删除操作
    
    删除p结点的后继结点
        q = p->next;
        p->next = q->next;
        q->next->prior = p;
        free(q);

循环链表

循环单链表

    尾结点next指针指向头结点
    当头结点的next指向头指针链表为空
    如果需要对表头和表尾进行操作可以将头指针设置为尾指针

循环双链表

    尾结点next指针指向头结点,头结点的prior指针指向尾结点
    当头结点prior或者next等于L,链表为空

静态链表

静态链表的作用是用来在java这类高级语言中模拟单链表

静态链表用数组来描述线性表的链式存储结构,结点的next指针指向下个结点的索引(游标)

typedef struct {
	ElemType Data;
	int next;
} SLinkList[MaxSize];

next==-1的结点是尾结点

栈的定义和基本操作

栈:一种线性表,只能在一端删除和插入,后进先出

栈顶:允许插入和删除的一端

栈底:不允许插入和删除的一端

空栈:不包含任何元素的空表

InitStack(*S) 初始化一个空栈
StackEmpty(S) 判断一个栈是否为空,true或者false
Push(*S, x) 进栈,如果S未满,x进入栈顶
Pop(*s, *x) 出栈,如果S非空,栈顶元素弹出,用x返回
GetTop(S, *x) 取栈顶元素,S非空,用x返回
ClearStack(*S) 销毁栈,并释放S

顺序栈

用一组地址连续的存储单元存放栈元素,指针top指向栈顶

typedef struct {
	ElemType data[MaxSize];
	int top;
} SqStack;

栈顶指针初始值:S.top==-1

栈顶元素:S.data[S.top]

进栈操作:S.data[++S.top] = e

出栈操作:S.data[S.top--]

栈空判断条件:S.top == -1

栈满判断条件:S.top == MaxSize - 1;

栈的长度:S.top + 1;

具体操作
    初始化
        void InitStack(SqStack *S){
            S.top=-1;
        }

    判断栈是否为空
        bool StackEmpty(SqStack S){
            if(S.top == -1) return true;
            return false;
        }

    进栈
        bool Push(SqStack *S, ElemType x){
            if(S->top == MaxSize - 1) return false;
            S->data[++S->top] = x;
            return true;
        }

    出栈
        bool Pop(SqStack *S, ElemType *x){
            if(S->top == -1) return false;
            x = S->data[S->top--];
            return true;
        }

    取栈顶元素
        bool GetTop(SqStack S, ElemType *x){
            if(S->top == -1) return false;
            x = S->data[S->top];
            return true;
        }

共享栈

为了更好的利用存储空间,将两个栈的栈底分别设置为一维数据空间的两端

top0为-1表示0号栈为空栈,top1为MaxSize表示1号栈为空栈

top1-top0为1表示栈满

0号栈入栈:++top0

1号栈入栈:--top1

链栈

链栈,其实就是单链表,只不过多了一个指针指向头结点

链栈使用单链表表示,不设头结点,Lhead表示栈顶指针

typedef struct LNode {
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

typedef struct {
    LinkList list;
    LNode head;
} LStack;

队列

队列的定义和基本操作

一种线性表,只能在一端进行插入,另一端进行删除,先进先出

对头:允许删除的一端

对尾:允许插入的一端

空队列:不含任何元素的空表

InitQueue(*Q) 初始化一个空队列Q
QueueEmpty(Q) 判断队列是否为空,true或false
EnQueue(*Q, x) 入队,x插入队尾
DeQueue(*Q, *x) 出对,删除对头元素,用x返回
GetHead(Q, *x) 获取对头元素

顺序队

分配一块连续的存储单元存放队列的元素,start指针指向对头,end指针指向队尾下一个位置

typedef struct {
    ElemType data[MaxSize];
    int start, end;
}SqQueue;

对空的条件:Q.start==Q.end==0

入队操作:Q.data[Q.end++]

出队操作:Q.data[Q.start++]

假溢出:入队出现上溢出,但是数组中任然可以存放元素,这时需要循环队列

循环队列

为了解决假溢出现象,可以将顺序队构造为一个环形的逻辑结构,称为循环队列

初始状态:Q.start==Q.end==0

入队:Q.end=(Q.end + 1)%MaxSize

出队:Q.start=(Q.start + 1)%MaxSize

队长:(MaxSize - Q.start + Q.end)%MaxSize

区分队满的三种方式

    牺牲一个存储单元,队尾指针下一个位置是队头指针表示队满

        队满条件:(Q.end+1)%MaxSize == Q.start
        队空条件:Q.start == Q.end
        队长:(Q.end + MaxSize - Q.start)%MaxSize

    队列存储结构中新增一个表示元素个数的成员size
    
        队空:Q.size==0
        队满:Q.size==MaxSize
        都满足Q.start == Q.end

    队列存储结构中新增一个tag

        删除元素同时将tag设为0,插入元素同时将tag设为1
        tag为0并且start==Q.end表示队空
        tag为1并且start==Q.end表示队满

相关操作实现

    初始化
        void InitQueue(*Q){
            Q.end = Q.start = 0;
        }

    判断队列是否为空
        bool isEmpty(Q) {
            if(Q.end == Q.start) return true;
            return false;
        }

    入队
        bool EnQueue(SqQueue *Q, ElemType x){
            if( (Q->end + 1)%MaxSize==Q->start ) return false;
            Q.data[Q->end] = x;
            Q.end = (Q->end + 1)%MaxSize;
            return true;
        }
    出对
        bool DeQueue(SqQueue *Q, ElemType *x){
            if(Q->end == Q->start) return false;
            x = Q->data[Q.start];
            Q->start = (Qstart + 1)%MaxSize;
            return true;
        }

链队

链队,其实是带头结点单链表,只不过具有队头指针和队尾指针,头指针指向对头结点,尾指针指向队尾结点

typedef struct {
    ElemType data;
    struct LNode *next;
}LNode;

typedef struct {
    LNode *start, *end;
}LQueue

队空:Q.start==Q.end;


相关操作实现

    初始化
        void InitQueue(LQueue *Q) {
            Q->start = Q->end = (LNode*)malloc(sizeof(LNode)); // 建立头结点
            Q->start->next = NULL;
        }

    判断队列是否为空
        bool IsEmpty(LQueue Q) {
            if(Q.start == Q.end) return true;
            return false;
        }

    入队
        void EnQueue(LQueue *Q, ElemType x){
            LNode *s = (LNode *)malloc(sizeof(LNode));
            s->data=x;
            s->next=NULL;
            Q->end->next=s;
            Q->end=s;
        }

    出队
        bool DeQueue(LQueue *Q, ElemType *x){
            if(Q->start == Q->end) return false;

            LNode *p = Q->start->next;
            x = p->data;

            if(p == Q->end){
                Q->end = Q->start;
            }else {
                Q->start->next = p->next;
            }

            free(p);
            return true;
        }

双端队列

双端队列:允许两端都可以进行入队和出队,分为前端和后端

输出受限的双端队列:某一端不允许删除的双端队列

输入受限的双端队列:某一端不允许插入的双端队列

栈底相邻的栈:      插入的元素只能在同一端删除的双端队列

栈和队列的应用

栈在括号匹配中的应用

若括号左右匹配则两个符号出栈
符号出现不合法,也就是右边比左边先出现,则不匹配
最后栈中还有符号,则不匹配

bool Match(char str[], int n) {
	int i = 0;
	char c,temp;
	SqStack stack;
	stack.top = -1;
	
	while(i < n) {
		c = str[i];
		
		switch(c){
			case '(':
			case '[':
			case '{':
				stack.data[++stack.top] = c;
				break;
			case ')':
				temp = stack.data[stack.top];
				if(temp == '('){
					--stack.top;
				}else{
					return false;
				}
				break;
			case ']':
				temp = stack.data[stack.top];
				if(temp == '['){
					--stack.top;
				}else{
					return false;
				}
				break;
			case '}':
				temp = stack.data[stack.top];
				if(temp == '{'){
					--stack.top;
				}else{
					return false;
				}
				break;
			default:
				break;
		}
		++i;
	}
	
	if(stack.top != -1) return false;
	return true;
}

栈在表达式求值中的应用

中缀表达式转换成后缀表达式的步骤
    优先级高的表达式先处理,将操作数放前面,运算符放后面,然后将此结果作为一个整体重复以上步骤
    如 A+B*(C-D) - E/F 转化后 ABCD-*+EF/-

    算法步骤:
        1.操作数直接输出,符号入栈
        2.优先级比栈顶元素高的符号压栈,否则弹栈,继续比较
        3.遇到括号入栈,遇到匹配的符号元素出栈

通过后缀表达式计算表达式值的步骤
    顺序扫描表达式每一项,操作数入栈
    扫描到符号,将栈顶两个操作数弹出做运算,结果再次入栈
    扫描完成之后,栈顶就是运算结果

栈在递归中的应用

系统为每一次递归函数开辟递归工作栈来存储数据,先执行的函数代码后执行

队列在层次遍历中的应用

二叉树层次遍历中应用到队列

数组

一维数组是由n个相同的数据元素构成的有限序列

n维数组是有n个线性关系约束的数组

数组除了初始化和销毁以外,只有存取元素和修改元素的操作

一维数组A[0...n-1]的存储结构关系式
    LOC(ai) = LOC(a0) + i*L 其中 0<= i <=n,L是每个数组元素所占存储单元

多维数组有两种映射方法
    按行优先,先存储行号小的元素,行号相同先存储列号小的元素
    按列优先,先存储列号小的元素,列号相同先存储行号小的元素

串的定义和存储结构

串是由零个或者多个字符组成的有序序列,串中字符的个数称为串的长度,含有零个元素的串叫空串

char str[] = "abcd";
数组str中存储的字符 'a' 'b' 'c' 'd' '\0' 
数组长度为5,串长度为4

定长顺序存储表示
    typedef struct {
        char str[MaxSize+1];
        int length; 
    } Str;

变长分配存储表示
    typedef struct {
        char *ch;
        int length;
    } Str;

串赋值操作

将Str2字符串赋值给Str1字符串

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
	char* StrCopy(char *Str2);
	char *Str1, *Str2="I am YeJiaWei";
	Str1 = StrCopy(Str2);
	printf("%s\n", Str1);
	return 0;
}

char* StrCopy(char *Str2) {
	int length = strlen(Str2), i = 0;
	if(length==0){
		return NULL; 
	}
	
	char *Str1;
	Str1 = (char *)malloc(sizeof(char)*(length+1));
	while(i < length){
		Str1[i]=Str2[i];
		++i;
	}
	Str1[length] = '\0'; 
	return Str1;
}

串的模式匹配

简单的模式匹配

子串中的字符依次和主串中的字符比较

算法时间复杂度 O(len1*len2)

查找Str2在Str1中匹配的第一个索引位置

    int StrMatch(char *Str1, char *Str2){
        int i = 0, j = 0, temp = 0, len1 = strlen(Str1), len2 = strlen(Str2);
        while(i<len1&&j<len2){
            if(Str1[i] == Str2[j]) {
                ++i;
                ++j;
            }else{
                j=0;
                i=++temp;
            }
        }
        if(j = len2) return temp;
        return -1;
    }

KMP算法

KMP算法,主串i不回溯

KMP算法的时间复杂度是O(len1+len2)

KMP算法实现的首要是求解next数组,也就是子串不匹配时回溯的位置
    求解next数组,找到每个位置的最长公共前缀
    最大公共前后缀是j-1长度子字符串,前端和后端最长匹配字符串

    next数组的规定
        当j=0时,规定next[j]为-1
        当j=1时,规定next[j]为0
        当没有最长公共前后缀时,规定next[j]为0
        如果最长公共前后缀长度为k,规定next[j] = k + 1

    求next数组算法描述
        当j=0时,规定next[j]为-1
        当j=1时,规定next[j]为0
        令k=next[j-1],相当于求j-1最大公共前后缀后一位字符的位置
        如果j-1对应的字符和k对应的字符相等,那么next[j] = k + 1
        如果j-1对应的字符和k对应的字符不相等,令k=next[k]
            如果k等于-1,next[j] = k + 1;
            如果k不等于-1,重复第四步

    求next数组代码实现
        i   表示上一个元素索引
        j   表示上一个元素的next值

        void GetNext(char *Str, int next[]){
            int i = 0, j = -1;
            next[0] = -1;
            while(i < strlen(Str)){
                if(j==-1 || Str[i] == Str[j]){
                    next[++i] = ++j;
                }else{
                    j = next[j];
                }
            } 
        }

KMP算法实现
    int main(void) {
        void GetNext(char *Str, int next[]);
        int KMP(char *Str1, char *Str2, int next[]);
        
        char *Str1 = "abaabaababaaaa";
        char *Str2 = "aababaa";
        
        int next[strlen(Str2)];
        GetNext(Str2,next);
        
        int index = KMP(Str1, Str2, next);
        printf("%d\n", index); // 5
        
        return 0;
    }

    int KMP(char *Str1, char *Str2, int next[]) {
        int i = 0, j = 0, len1 = strlen(Str1), len2 = strlen(Str2);
        while(i < len1 && j < len2){
            if(j == -1 || Str1[i] == Str2[j]) {
                ++i;
                ++j;
            }else{
                j=next[j];
            }
        }
        if(j == len2) return i-len2;
        return -1;
    }

KMP的最坏情况
    aaabaaabaaabaaaa 主串
    aaaa 模式串
    这种情况下的KMP性能最低,下面的改良KMP就是要解决这种情况

改良的KMP算法

void GetNext(char *Str, int next[]){
	int i = 0, j = -1;
	next[0] = -1;
	while(i < strlen(Str)){
		if(j==-1 || Str[i] == Str[j]){

            // 如果字符和自己对应的next字符相等的话,下次匹配的时候就没必要再比较了,直接取取字符next的next值即可
			if(Str[++i] == Str[++j]){
				next[i] = next[j];
			}else{
				next[i] = j;
			}
			
		}else{
			j = next[j];
		}
	} 
}

树和二叉树

树的基本概念

定义
    树是一种逻辑结构,树是N个结点的有限集合,N为0表示空树
    子树之间互不相交
    树可以表示递归和分层的逻辑关系
    树中的结点只有一个前驱结点,可以有多个后继结点
    n个结点的树,有n-1个分支

术语
    祖先结点:结点到根节点路径之间的所有结点
    子孙结点:结点到子结点路径之间的所有结点
    双亲结点:最近的祖先结点
    孩子结点:祖先结点的直接孩子
    兄弟结点:双亲相同的结点
    结点的度:一个结点拥有的孩子的个数
    树的度  :结点度的最大值
    分支结点:度不为0的结点
    叶子结点:度为0的结点
    结点层次:根节点为第1层
    结点深度:就是结点的层次
    结点高度:就是当前结点到叶子结点的层数
    树的高度:或者称为深度,树中最大的层数
    有序树和无序树:结点的子树是有顺序的不能交换,反之称为无序树
    路径    :祖先结点到子孙结点之间的所经过的结点序列叫做路径
    路径长度:路径上经过的边的个数,也就是从一个结点到另一个结点要走几步
    树的路径长度:所有结点路径长度的总和
    森林    :去掉树根结点,就称为森林,深林由若干互不相交的子树构成

树的性质
    树中结点数等于所有结点度数加1
    度为m的树中第i层上最多有m的i-1次方个结点
    高度为n的m叉树,最多有(mⁿ-1)/(m-1)个结点
    具有s个结点的m叉树,最小高度n直接对s=(mⁿ-1)/(m-1)求对数向上取整即可

二叉树的定义和存储结构

定义
    二叉树每个结点最多只有两个子树
    二叉树子树有左子树和右子树之分
    二叉树结点次序是确定的,不是相对于另一个结点

特殊的二叉树
    满二叉树
        除叶子结点外,每一个结点的度数都是2
        第i层结点的个数为2的i-1次方
        度为h的二叉树结点的个数为2的h次方加1
        对满二叉树自上而下,从左到右编号,编号为i的结点,双亲编号为i/2向下取整,左孩子编号2*i,右孩子编号2i+1
    完全二叉树
        完全二叉树每一个结点的编号和满二叉树对应的结点相同
        编号为i的结点,如果i小于或等于n/2向下取整,则i为分支结点,否则为叶子结点
        叶子结点只可能出现在层次最大的两层上,最大层次的叶子结点都在最左边,其次层次在最右边
        度为1的结点只可能有一个,并且这个结点只有左孩子
        层序编号后的结点,只要i为叶子结点或者只有左孩子,那么其后的结点都是叶子结点
        若n为奇数,则完全二叉树每个分支结点都是满的,如果n是偶数,n/2这个分支结点只有左孩子
    二叉排序树,左子树结点值小于根结点值,右子树结点值大于根结点值
    平衡二叉树,二叉排序树左子树和右子树深度差不超过1

二叉树的性质
    非空二叉树叶子结点树等于度为2结点数加1,N0=N2+1
    非空二叉树第k层最多的结点数为2的k-1次方
    高度为H的二叉树最多有2的H次方-1个结点
    对完全二叉树编号
        i>1,双亲结点编号为i/2向下取整
        2i<=N,结点i的左孩子编号为2i,否则没有孩子
        2i+1<=N,结点i的右孩子编号为2i+1,否则没有右孩子
        结点i所在的层次为
            log以2为底i+1的对数并且向上取整
            log以2为底i的对数并且向下取整加1

二叉树的存储结构
    顺序存储结构,适用于完全二叉树和满二叉树
        使用一组地址连续的存储单元依次从上到下,从左到右存储完全二叉树的结点元素,数组的下标反映结点之间的关系
        对于一般的二叉树,只能添加一些不存在的空节点让其与完全二叉树上的结点对应,然后再存储到一维数组中
        存储元素必须从数组下标1的位置开始存储

    链式存储结构,适用于一般的二叉树
        使用一个链表来存储一个二叉树
        二叉链表存储结构描述
            typedef struct BiTNode {
                ElemType data;
                struct BiTNode *lchild, *rchild;
            } BiTNode, *BiTree
        在含有n个结点的二叉链表中有2n-(n-1)个空链域

二叉树递归方式遍历

三种方式时间复杂度都是O(n),空间复杂度O(n)

先序遍历
    void PreOrder(BiTree tree) {
        if(tree != NULL){
            printf("%d\n", tree->data);
            PreOrder(tree->lchild);
            PreOrder(tree->rchild);
        }
    }

中序遍历
    void InOrder(BiTree tree) {
        if(tree != NULL){
            PreOrder(tree->lchild);
            printf("%d\n", tree->data);
            PreOrder(tree->rchild);
        }
    }

后序遍历
    void PoOrder(BiTree tree) {
        if(tree != NULL){
            PreOrder(tree->lchild);
            PreOrder(tree->rchild);
            printf("%d\n", tree->data);
        }
    }

二叉树非递归方式遍历

自定义栈实现
#define ElemType int
#define MaxSize 50

typedef struct BiTNode {
	ElemType data;
	struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

typedef struct {
	struct BiTNode *data[MaxSize];
	int top;
}SqStack;

前序遍历
    void PreOrder(BiTree tree) {
        SqStack stack;
        BiTNode *temp = tree;
        stack.top = -1;
        stack.data[++stack.top] = temp;
        
        while(stack.top != -1){
            temp = stack.data[stack.top--];
            printf("%d\n", temp->data);
            if(temp->rchild != NULL) {
                stack.data[++stack.top] = temp->rchild;
            }
            
            if(temp->lchild != NULL) {
                stack.data[++stack.top] = temp->lchild;
            }
        }
    }

中序遍历
    void InOrder(BiTree tree) {
        SqStack stack;
        stack.top = -1;
        
        BiTNode *temp = tree;
        
        while(stack.top != -1 || temp != NULL) {
            while(temp != NULL) {
                stack.data[++stack.top] = temp;
                temp = temp->lchild;
            }
            
            if(stack.top != -1){
                temp = stack.data[stack.top--];
                printf("%d\n", temp->data);
                temp = temp->rchild;
            }
        }
    }

后序遍历
    void PoOrder(BiTree tree) {
        SqStack stack1, stack2;
        stack1.top = -1;
        stack2.top = -1;
        BiTNode *temp = tree;
        stack1.data[++stack1.top] = temp;
        
        while(stack1.top != -1) {
            temp = stack1.data[stack1.top--];
            stack2.data[++stack2.top] = temp;
            if(temp->lchild != NULL) {
                stack1.data[++stack1.top] = temp->lchild;
            }
            
            if(temp->rchild != NULL) {
                stack1.data[++stack1.top] = temp->rchild;
            }
        }
        
        while(stack2.top != -1){
            temp = stack2.data[stack2.top--];
            printf("%d\n", temp->data);
        }
        
    }

层次遍历,按层遍历
    void LevelOrder(BiTree tree) {
        SqQueue queue;
        queue.start = queue.end = 0;
        BiTNode *temp = tree;
        queue.data[queue.end] = temp;
        queue.end = (queue.end + 1)%MaxSize;
        
        while(queue.start != queue.end) {
            temp = queue.data[queue.start];
            queue.start = (queue.start + 1)%MaxSize;
            printf("%d\n", temp->data);
            if(temp->lchild != NULL) {
                queue.data[queue.end] = temp->lchild;
                queue.end = (queue.end + 1)%MaxSize;
            }
            
            if(temp->rchild != NULL) {
                queue.data[queue.end] = temp->rchild;
                queue.end = (queue.end + 1)%MaxSize;
            }
        }
    }

由遍历的结果还原二叉树的注意点
    由二叉树的先序序列或者后序序列和中序序列可以唯一的确定一颗二叉树
    仅由先序序列和后序序列无法唯一确定一颗二叉树
    先序序列的第一个结点必定是根结点
    后序序列最后一个结点必定是根结点
    中序序列的根节点左边必定是左子树右边必定是右子树

线索二叉树的定义和存储结构

二叉树的遍历就是线性化的过程,二叉树中有N+1个空指针,可以用这些指针存放前驱和后继,从而快速查找前驱结点和后继结点
无左子树,lchild用来存放前驱结点,ltag设为1
无右子树,rchild用来存放后继结点,rtag设为1

存储结构描述
    typedef struct BiTNode {
        ElemType data;
        struct BiTNode *lchild, *rchild;
        int ltag, rtag;
    }BiTNode, *BiTree;
指向前驱和后继的指针叫做线索

线索二叉树的建立

对现成的二叉树进行遍历,判断左右指针域是否为空

前序遍历方式建立前序线索二叉树

    void PreOrderCreate(BiTNode *currentNode, BiTNode *previousNode) {
        if(currentNode != NULL){
            if(currentNode->lchild != NULL) {
                currentNode->ltag = 0;
            }else{
                currentNode->lchild = previousNode;
                currentNode->ltag = 1;
            }
            
            if(previousNode != NULL){
                if(previousNode->rchild != NULL) {
                    previousNode->rtag = 0;
                }else{
                    previousNode->rchild = currentNode;
                    previousNode->rtag = 1;
                }
            }
            
            previousNode = currentNode;
            
            if(currentNode->ltag == 0) {
                PreOrderCreate(currentNode->lchild, previousNode);
            }
            if(currentNode->rtag == 0) {
                PreOrderCreate(currentNode->rchild, previousNode);
            }
        }
    }

中序遍历方式建立中序线索二叉树

    void InOrderCreate(BiTNode *currentNode, BiTNode *previousNode) {
        if(currentNode != NULL){
            
            InOrderCreate(currentNode->lchild, previousNode);
            
            if(currentNode->lchild != NULL) {
                currentNode->ltag = 0;
            }else{
                currentNode->lchild = previousNode;
                currentNode->ltag = 1;
            }
            
            if(previousNode != NULL){
                if(previousNode->rchild != NULL) {
                    previousNode->rtag = 0;
                }else{
                    previousNode->rchild = currentNode;
                    previousNode->rtag = 1;
                }
            }
            
            previousNode = currentNode;
            printf("%d, %d\n", previousNode->data,previousNode->rtag);
            
            InOrderCreate(currentNode->rchild, previousNode);
        }
    }

后序遍历方式建立后序线索二叉树
    
    void PoOrderCreate(BiTNode *currentNode, BiTNode *previousNode) {
        if(currentNode != NULL){
            
            PoOrderCreate(currentNode->lchild, previousNode);
            PoOrderCreate(currentNode->rchild, previousNode);
            
            if(currentNode->lchild != NULL) {
                currentNode->ltag = 0;
            }else{
                currentNode->lchild = previousNode;
                currentNode->ltag = 1;
            }
            
            if(previousNode != NULL){
                if(previousNode->rchild != NULL) {
                    previousNode->rtag = 0;
                }else{
                    previousNode->rchild = currentNode;
                    previousNode->rtag = 1;
                }
            }
            
            previousNode = currentNode;
        }
    }

线索二叉树的遍历


前序线索二叉树遍历

    void preOrder(BiTree tree) {
        BiTNode *temp = tree;
        
        if(temp != NULL) {
            while(temp != NULL) {
                while(temp->ltag == 0){
                    printf("%d\n", temp->data);
                    temp = temp->lchild;
                }
                printf("%d\n", temp->data);
                temp = temp->rchild;
            }	
        }
        
    }

中序线索二叉树遍历

    void Order(BiTree tree) {
        BiNode *node = tree;
        
        if(node != NULL) {
            while(node->ltag == 0) {
                node = node->lchild;
            }
            
            while(node != NULL) {
                printf("%d\n", node->data);
                
                if(node->rtag == 0) {
                    node = node->rchild;
                    while(node->ltag == 0) {
                        node = node->lchild;
                    }
                }else{
                    node = node->rchild;
                }
            }
        }
        
    }

后序线索二叉树遍历

    过于复杂,实用价值太低,暂不介绍

树的其他存储结构

双亲表示法
    采用一组连续空间来存储树的每个结点,结点中增加一个指针指向其双亲在数组中的位置
    树根结点指针为-1
    存储结构表示
        #define MAX_TREE_SIZE 100
        typedef struct {
            ElemType data;
            int parent;
        }PTNode;
        typedef struct {
            PTNode nodes[MAX_TREE_SIZE];
            int n; // 结点数
        }PTree;
    这种方式很容易找到双亲结点,孩子结点需要遍历整棵树

孩子表示法
    将每个结点的孩子结点都用单链表存储,N个结点就有N个单链表
    寻找子女结点很方便,但是寻找双亲结点就要遍历N个结点的N个链表

孩子兄弟表示法
    将树转化成二叉树,使用二叉链表表示存储结构,每个结点包含指向第一个孩子结点的指针和指向下一个兄弟结点的指针
    可以方便的查找孩子结点和兄弟结点,但是查找双亲很烦,可以额外再增加一个parent指向其双亲结点
    孩子兄弟法存储结构
        typedef struct CSNode {
            ElemType data;
            struct CSNode *firstchild, *nextsibling;
        }CSNode, *CSTree;

树和二叉树的转化

将树转换成二叉树就是孩子兄弟表示法,不再赘述

森林转换成二叉树
    将每一颗树分别转换成二叉树
    第二颗树作为第一颗树根的右子树,第三颗树作为第二颗树的右子树,依此类推

二叉树转换成森林
    将二叉树右子树当做新的二叉树,依次类推,然后将每个子二叉树转换成树即可

树和森林的遍历

树的遍历方式
    先根遍历
        先访问根结点,然后从左到右依次遍历根结点的子树,访问的顺序和对应的二叉树先序遍历相同
    后根遍历
        先遍历结点的左右子树,然后再访问结点,访问的顺讯和对应的二叉树的后序遍历相同

森林的遍历方式
    先序遍历,先根遍历每一颗子树
    中序遍历(后序遍历),后根遍历每一颗子树

二叉排序树

简称BST,左子树小于根节点,右子树大于根结点

二叉排序树的查找

    二叉排序树的查找:结点等于要查找的值则查找成功,小于则去左子树查找,大于则去右子树查找,其实就是二分查找
    二叉排序树的平均查找长度,取决于树的高度
        查找成功的平均查找长度为,所有结点查找路径和的平均值
        查找失败的平均查找长度为,所有查找不成功结点的父节点查找路径和的平均值

        非递归算法

            BiTNode* BSTSearch(BiTree tree, ElemType item){
                BiTNode *temp = tree;
                
                while(temp != NULL && item != temp->data){
                    if(item < temp->data){
                        temp = temp->lchild;
                    }else {
                        temp = temp->rchild;
                    }
                }
                return temp;
            }

        递归算法

            BiTNode* BSTSearch(BiTNode *node, ElemType item){
                if(node == NULL) return NULL;
                if(node->data == item){
                    return node;
                }else if(node->data > item) {
                    return BSTSearch(node->lchild, item);
                }else if(node->data < item){
                    return BSTSearch(node->rchild, item);
                }
            }

二叉排序树的插入

    插入的新结点必定是叶子结点
    高度为H的二叉排序树,插入操作的时间复杂度是O(H)

    bool BSTInsert(BiTNode **node, ElemType item){
	 
        if(*node == NULL){
            *node=(BiTNode *)malloc(sizeof(BiTNode));
            
            (*node)->data=item;
            (*node)->lchild=(*node)->rchild=NULL;
            return true;
        }else{
            if((*node)->data == item) {
                return false;
            }else if((*node)->data > item) {
                return BSTInsert(&((*node)->lchild), item);
            }else if((*node)->data < item){
                return BSTInsert(&((*node)->rchild), item);
            }                 
            
        } 
        
    }
    
利用插入算法构建二叉排序树

    如果构造二叉排序树的序列是有序的,会形成单边树,树的高度为O(n),性能最差

    void CreateTree(BiTNode **node, ElemType data[], int n) {
        *node = NULL;
        int i = 0;
        bool BSTInsert(BiTNode **node, ElemType item);
        while(i<n){
            BSTInsert(node, data[i]);
            ++i;
        }
    }

二叉排序树的删除

    高度为H的二叉排序树,删除操作的时间复杂度是O(H)

    如果删除的是叶子结点,直接删除即可
    如果删除的结点只有一个子树,直接将子树替换结点位置即可
    如果删除的结点有左右子树,将此结点用中序遍历的前驱或者后继结点取代,然后就转化成第一种和第二种情况了

平衡二叉排序树


如果左右子树高度差不超过1,这种树叫做平衡二叉树,平均查找长度O(log₂n)

简称AVL树,在插入和删除结点时保证任意结点左右子树高度差不超过1

高度差称为结点的平衡因子,只能为 -1,0,1

平衡二叉树的插入
    因为插入结点导致不平衡,需要找到离插入结点最近的平衡因子绝对值大于1的结点A,然后调整此结点A
    这个结点A代表的树叫做最小不平衡子树
    LL平衡旋转
        在A结点左孩子的左子树上插入结点导致A失去平衡,此时调整如下
        将A结点作为左孩子的右结点,左孩子的原右结点作为A的左结点
    RR平衡旋转
        在A结点右孩子的右子树上插入结点导致A失去平衡,此时调整如下
        将A结点作为右孩子的左结点,右孩子的原左结点作为A的右结点
    LR平衡旋转
        在A结点左孩子的右子树上插入结点导致A失去平衡,此时调整如下
        先对A结点的左孩子进行RR平衡旋转,然后对A结点进行LL平衡旋转
    RL平衡旋转
        在A结点右孩子的左子树上插入结点导致A失去平衡,此时调整如下
        先对A结点的右孩子进行LL平衡旋转,然后对A结点进行RR平衡旋转
平衡二叉树的查找
    平衡二叉树的查找和二叉排序树的查找类似
    n个结点的平衡二叉树的最大深度为log₂n向上取整

赫夫曼树和赫夫曼编码

哈夫曼树
    结点带有某种意义的值,称为结点的权
    路径长度和结点权值的乘积就是此结点的带权路径长度
    所有叶子结点的带权路径长度和为该树的带权路径长度,即为WPL
    WPL最小的带权二叉树称为哈夫曼树,也就是最优二叉树
    哈夫曼树的构造,构造一个新的结点,选取最小权值的两个结点作为它的左右子树,此结点的权值为两者的和,重复此步骤

哈夫曼编码
    对一个字符串的字符使用同样长度的二进制位表示,这种编码方式为固定长度编码
    可变长度编码,使用不等长的二进制位表示,对频率高的字符用短编码,频率低的字符用长编码
    没有编码是另一个编码的前缀,称为前缀编码
    哈夫曼编码就是一种前缀可变长度编码,对数据压缩非常有效
    由哈夫曼树构造哈夫曼编码
        将每个出现的字符当做一个独立的结点,权值为出现的次数,构造哈夫曼树,左边记为0右边记为1即可得到哈夫曼编码

查找

查找的基本概念

查找:寻找满足条件的数据元素的过程,要么查找成功要么查找失败

查找表(查找结构): 用于查找的数据集合

静态查找表:一个查找表的操作只涉及,查询某个数据元素是否在表中或者检索某个数据元素的各种属性
适合此类的查找方法有,顺序查找,折半查找,散列查找等

动态查找表:一个查找表的操作涉及,动态的插入和删除
适合此类的查找方法有,二叉排序树查找,散列查找

关键字:数据元素中唯一标识

平均查找长度:查找过程中进行比较次数的平均值

ASL
    ASL是衡量查找算法效率的最主要的指标

    一般的,ASL为Pi*Ci求和,其中i从1到n,pi是查找第i个元素的概率,一般Pi为1/n,Ci是查找这个元素所要比较的次数

顺序查找

从线性表一端开始逐个查找关键字是否满足给定条件

无序表的顺序查找
    int Search(SqList list, ElemType item) {
        int i = -1;
        while(i++ < list.length) {
            if(list.data[i] == item) {
                return i;
            }
        }
        return -1;
    }

    ASL分析
        查找成功的ASL
            Pi = 1/n
            Ci = i
            则ASL为i/n的和,其中i从0到n-1,即(n-1)/2

        查找不成功的ASL
            Pi = 1/n
            Ci = n-1
            则ASL为n-1

有序表的顺序查找
    如果线性表是有序的,那么失败就不需要到表的另一端,可以降低失败的平均查找长度
    假设表是升序排序的,如果当前判断的结点不相等并且查找值小于结点值,则可以判断查找失败
    查找成功的ASL和一般线性表的顺序查找相同
    查找失败的ASL
        Pi=1/n;
        ci=i
        ASL = (n-1)/2

折半查找

思想类似于二叉排序树查找,仅仅适用于有序的顺序表

int Search(SqList list, ElemType item) {
	int start = 0, end = list.length - 1, temp;
	while(start <= end) {
		temp = (start + end) / 2;
		if(list.data[temp] == item) {
			return temp;
		}else if(list.data[temp] > item) {
			end = temp-1;
		}else{
			start = temp + 1;
		}
	}
	return -1;
}

判定树
    构造判定树
        将中间结点作为树,小的结点作为左子树,大的结点作为右子树,使用圆形结点表示
        叶结点为查找不成功的情况,使用方形结点表示
        若判定的有序序列有n个元素,那么构造出来的判定树有n个圆形结点和n+1个方形结点

    查找成功时的查找长度为从根结点到目标结点的结点数
        ASL = (1*1 + 2*2 + ... + h*2^h-1)/n = (n+1)log₂(n+1)-1/n ≈ log₂(n+1)-1
        时间复杂度即O(log₂n)

    查找不成功时的查找长度为从根结点到方形结点的父节点的结点数

    判定树是一颗平衡二叉树
    表长为n的有序表生成的判定树的高度为log₂(n+1)向上取整

分块查找(索引查找)

将查找表分成若干子块,块内元素无序块间有序,第一个子块中的最大关键字小于第二个块中的所有关键字,依次类推

需要新建一个索引表,索引表中每一个元素含有块中最大值和块第一个元素的地址,索引表有序排列

查找过程,先在索引表中查找记录在哪个块中,可以采取顺序查找和折半查找,然后在块内用顺序查找

平均查找长度ASL的分析
    设索引查找ASL为L1,块内查找ASL为L2
    ASL = L1 + L2;
    将长度为n的查找表分为a块,每块有b个记录
    索引表和块内都采用顺序查找
        ASL = (a+1)/2 + (b+1)/2 = 1 + (b²+n)/2b
        当b取n的开方,平均查找长度最小
    索引表采用折半查找块内采用顺序查找
        ASL = log₂(a+1) - 1 + (b+1)/2

B树(多路平衡查找树)

基本概念
    结点孩子结点数的最大值称为B树的阶,用m表示

    m阶B树具有以下特性
        根结点有孩子,则至少有2个
        每个结点最多有m颗子树,即m-1个关键字
        除根结点外非叶结点至少有,m/2向上取整颗子树,即m/2向上取整减1个关键字
        非叶结点的构造 n p0 k1 p1 k2 p2 ... kn pn
            ki为结点的关键字,i从1到n,k1<k2<...<kn
            pi为指向子树的指针,pi-1子树所有结点的关键字小于ki,pi子树所有结点的关键字大于ki
            n为结点中关键字的个数
        所有的叶子结点都在同一层上,相当于查找失败的结点,一共n+1个结点
        所有结点平衡因子为0

B树的查找
    先在B树中找结点然后在结点内找关键字如果没找到根据对应区间内的指针到子树中查找
    如果找到叶结点,即对应的指针为空指针则查找失败

B树的插入
    定位:找出插入该关键字最底层中某个非叶结点
    插入:如果插入关键字后结点的关键字总量超过m-1个,必须进行分裂
    分裂:
        第一步:将带插入的关键字按升序放入结点类
        第二步:取出m/2向上取整位置的关键字,将此关键字放到父结点中
        第三步:剩余两部分,左边不动,右边作为此关键字下个指针的孩子
        第四步:如果父结点也超出范围了,那么重复此步骤,直到根结点为止,此时B树高度加1

B树的删除
    如果删除的关键字在终端结点,也就是最底层的非叶结点中
        删除后关键字个数不少于m/2向上取整减1,直接删除即可

        兄弟够借
            删除的关键字所在的结点个关键字个数等于m/2向上取整减1,其左或者右兄弟结点的关键字个数大于m/2向上取整减1
            将左兄弟最后一个关键字或者右兄弟第一个关键字放到父关键字位置,然后将父关键字放到删除的关键字位置

        兄弟不够借
            删除的关键字所在的结点个关键字个数等于m/2向上取整减1,其左或者右兄弟结点的关键字个数也是等于m/2向上取整减1
            直接删除关键字,然后将剩下的关键字和左兄弟结点或者右兄弟结点和父关键字合并,观察父结点是否满足条件否则重复上述步骤

    如果删除的关键字不在终端结点
        当小于关键字的子树的关键字数大于m/2向上取整减1,那么直接找到此子树最后一个关键字替换即可
        当大于关键字的子树的关键字数大于m/2向上取整减1,那么直接找到此子树开始的一个关键字替换即可
        如果左右子树的关键字数都等于m/2向上取整减1,那么直接删除此关键字然后合并左右子树

B+树

B树的变形树,用于数据库数据存储

基本概念,m阶B+树
    每个分支结点最多有m颗子树
    非根结点至少两颗子树,其他的分支结点最少m/2向上取整颗子树,也就是一个关键字对应一颗子树
    结点的子树个数和关键字的个数相等
    分支结点中只包含各个子结点中关键字的最大值和指向其子结点的指针
    所有的叶子结点包含全部的关键字和指向相应记录的指针,叶子结点中的关键字有序排列,相邻的叶子结点按照顺序相互链接起来

B+树中有两个指针,一个指针指向根结点,一个指针指向最小的叶结点
B+树可以从根结点开始多路查找,也可以从叶结点开始顺序查找
多路查找,在非叶结点上匹配成功查找不会终止,一直到叶节点为止

散列表

基本概念
    散列函数:将关键字和关键字的地址之间建立函数,Hash(key) = addr; 如果多个key对应一个addr,就出现了冲突,那么这些key就是同义词
    散列表:根据关键字直接进行访问数据的数据结构,时间复杂度为O(1)

散列函数的构造方法
    直接定址法:
        直接取关键字的某个线性函数作为散列地址,Hash(Key) = a*key + b;
        其中a和b是常数,这种方法不会产生冲突,只适合关键字是基本连续的情况,否则空位较多

    除留余数法:
        Hash(key) = key%p;
        其中p最好是不大于并且接近表长的质数

    数学分析法:
        选取关键字key数位上分布比较均匀的几位作为散列地址
        只适合关键字已知

    平方取中法:
        求关键字的平方值,并且取中间几位作为散列地址
        适用于,关键字每一位数都不均匀或者关键字比散列地址短

    折叠法:
        将关键字分割成位数相同的几部分,取这些部分的叠加和作为散列地址
        适用于,关键字位数很多并且关键字每一位数分布大致均匀

处理冲突的方法
    开放定址法
        表中的空闲地址,新数据项可以用,同义词也可以用
        
        数学递归公式,Hi为第i次寻找同义词地址 Hi = ( Hash(key) + di ) % m; 其中i = 1,2,3,...,k (k<=m-1)
        di为增量序列,由增量序列引出如下几种方法:

        线性探测法
            增量序列 di = 1,2,...,m-1
            当出现同义词就会找下一个地址,直到找到一个空闲的存储单元
            同义词会造成原本没有同义词的关键字产生冲突,称为堆积问题

        平方探测法(二次探测法)
            增量序列 di = 1², -1², 2², -2², ... , k², -k², 其中 k <= m/2,m可以表示成4k+3的质数
            可以避免出现堆积问题,不能探测所有的单元,至少能探测一半

        再散列法(双散列法)
            增量序列 di = Hash2(key)

        伪随机序列法
            增量序列 di = 伪随机数序列
    链地址法
        将所有的同义词都存储到一个链表中,散列表中存储链表的地址

散列表的性能分析
    散列表的查找效率取决于三个因素
        散列函数
        处理冲突的方法
        装填因子α:
            表示一个表的装满程度,α = 表中的记录数n/散列表的长度m
            α越大,发生冲突的可能性就越大
            散列表的平均查找长度依赖于散列表的装填因子α,而不直接依赖于n或者m

排序

排序的基本概念

排序:重新排列表中的元素,使表中的元素满足按关键字递增或者递减

算法的稳定性:如果有两个元素,其关键字是相等的,通过排序算法之后,元素先后顺序改变了,则称这个排序算法是不稳定的,否则是稳定的

内部排序:排序期间元素全部都在内存中

外部排序:排序期间元素不断的在内、外存之间移动

插入类排序

直接插入排序

将带排序的关键字插入到已排序的子序列中合适位置

每次将一个待排序的记录按照关键字大小插入到前面已经排好序的子序列中

void Sort(int arr[], int n){
	int i, j, temp;
	
	for(i = 0; i < n;i++){
		j = i - 1;
		temp = arr[i];
		while(j >= 0 && temp < arr[j]){
			arr[j+1] = arr[j];
			--j;
		}
		arr[j+1] = temp;
	}
}

算法性能
    空间复杂度 O(1)

    时间复杂度
        最好情况:表中已经有序,子序列不需要移动,O(n)
        最坏情况:待排序的元素和已排好序的子序列是逆序,总的比较次数为i求和,i从0到n-1,所以时间复杂度O(n²)
        平均情况下,时间复杂度为O(n²)

    直接插入排序是稳定的,适用于顺序存储和链式存储的线性表,当表基本有序,数据量不是很大时性能最好

折半插入排序

此算法只适用于顺序存储的线性表

直接插入排序总是边比较边移动元素,折半插入排序是先找出要插入的位置,然后再统一的移动

void Sort(int arr[], int n){
	int i, j, temp, start, end, mid;
	
	for(i = 0; i < n; i++) {
		start = 0;
		end = i - 1;
		
		while(start <= end){
			mid = (start + end) / 2;
			if(arr[i] < arr[mid]){
				end = mid - 1;
			}else {
				start = mid + 1; 
			} 
		}
		
		temp = arr[i];
		
		for(j = i - 1; j >= end + 1; j--){
			arr[j+1] = arr[j];
		}
		arr[end + 1] = temp;
	}
}

算法性能
    空间复杂度:O(1)

    时间复杂度
        最好情况:元素不需要移动,时间复杂度就为元素查找插入位置的折半比较次数O(nlog₂n)
        最差情况:已排序序列需要整体移动,时间复杂度O(n²)
        平均情况:O(n²)

    折半插入排序也是一个稳定的排序方法

希尔排序

缩小增量排序,此算法只适用于顺序存储的线性表

取一个步长d,将待排序表分割成形如 L[i, i+d, i+2d, ..., i+kd]的子表,对这些子表进行直接插入排序
然后再缩小步长d,重复上述步骤,直到整个表基本有序,最后再来一次直接插入排序
希尔提出的步长缩减方式,d1 = n / 2, d2 = n / 4 ...

void Sort(int arr[], int n){
	int i, j, temp, step = 1;
	
	for(step = n/2; step >= 1; step = step/2){
		for(i = step; i < n; i++){
			if(arr[i] < arr[i-step]){
				temp = arr[i];
				j = i - step;
				
				while(j >= 0 && temp < arr[j]){
					arr[j+step] = arr[j];
					j -= step;
				}
				
				arr[j+step] = temp;
			}
		}
	}
}

算法性能
    空间复杂度:O(1)
    时间复杂度:最坏情况下的时间复杂度O(n²)

希尔排序是不稳定的

交换类排序

冒泡排序

从后往前两两比较元素的值,逆序则交换,一趟冒泡结束后最小的元素到表头,也就是最终的位置上
长度为n的表,最多进行n-1趟排序

void Sort(int arr[], int n){
	int i, j, temp;
	bool flag;
	
	for(i = 0; i < n - 1; i++){
		flag = false;
		
		for(j = n - 1; j > i; j--){
			if(arr[j-1] > arr[j]){
				temp = arr[j];
				arr[j] = arr[j-1];
				arr[j-1] = temp;
				flag = true;
			}
		}
		
		if(flag = false) return;
	}
	
}

算法性能
    空间复杂度:O(1)

    时间复杂度:
        最好情况下:序列本来就是有序的,比较次数n-1,时间复杂度O(n)
        最坏情况下:序列是逆序的,需要比较n-1趟,每一趟比较n-i次,时间复杂度为O(n²)

    冒泡排序是稳定的排序方法

快速排序

一般选取序列第一个元素作为枢轴,通过一趟排序将表中的小的元素放左边,大的元素放右边分成两个独立的部分
中间的位置就是枢轴的最终位置,然后对子表不断重复上述步骤

void Sort(int arr[], int start, int end){
	int temp, i = start, j = end;
	
	if(i < j) {
		temp = arr[i]; // 枢轴
		
		while(i < j) {
			while(i < j && arr[j] >= temp){ // 找到小于枢轴的值 
				--j; 
			}
			
			if(i < j) {
				arr[i] = arr[j];
				++i;
			}
			
			while(i < j && arr[i] < temp) {
				++i;
			} 
			
			if(i < j) {
				arr[j] = arr[i];
				--j;
			}
		}
		
		arr[i] = temp;
		
		Sort(arr, start, i - 1);
		Sort(arr, i + 1, end);
	}
	
}

算法性能
    空间复杂度:O(log₂n)

    时间复杂度:
        最坏情况:表本身就是有序的,时间复杂度就是O(n²)
        最好情况:当枢轴刚好处于序列的中间,O(nlog₂n)

    快速排序是不稳定的

选择类排序

简单选择排序

选取待排序序列中最小的关键字和待排序序列的第一个元素交换,重复这种操作

void Sort(int arr[], int n){
	int i, j, k, temp;
	
	for(i = 0; i < n; i++){
		k = i;
		
		for(j = i + 1; j < n; j++){
			if(arr[k] > arr[j]){
				k = j;
			}
		}
		
		temp = arr[i];
		arr[i] = arr[k];
		arr[k] = temp;
	}
}

算法性能
    空间复杂度:O(1)
    时间复杂度:O(n²)

堆排序

堆是一种数据结构,可以看成是一个完全二叉树

其中满足根结点值不小于左右孩子值的堆称为大顶堆,根结点值不大于左右孩子的值的称为小顶堆
大顶堆中,最大的元素存放在根结点中;小顶堆中,最小的元素存在根结点中
将一个无序的序列调整为一个堆,找到最值,然后与序列最后或者最前交换,对新的无序序列再次重复操作

注意:顺序表的索引必须从1开始

向下调整大顶堆
    void HeapAdjust(int arr[], int start, int end) {
        int temp, i = start, j = 2 * start; // arr[j]是arr[i]的左孩子
        
        temp = arr[i];
        
        while(j <= end) {
            if(j < end && arr[j] < arr[j+1]){ // 如果有右子树,跟右子树比较,将j赋值为大的索引
                ++j;
            }
            
            if(temp < arr[j]) { // 父结点比孩子结点小,将父结点赋值为孩子
                arr[i] = arr[j];
                i = j;
                j = 2*j;
            }else {
                break;
            }
        }
        
        arr[i] = temp;
    }

堆排序

    void Sort(int arr[], int n){
        void HeapAdjust(int arr[], int start, int end);
        int i, temp; 
        for(i = n / 2; i >= 1; i--) { // 建立初始堆,i = n/2 取的是最后一个叶子结点的父节点
            HeapAdjust(arr, i, n);
        }
        
        for(i = n; i >= 2; i--){
            temp = arr[1]; // 大顶堆根结点,也就是最大值
            arr[1] = arr[i]; 
            arr[i] = temp;
            
            HeapAdjust(arr, 1, i-1);
        }
    }

堆的其他操作
    插入结点:将插入的结点放到最底层的最右边,然后向上调整
    删除结点:将堆的最底层的最右边的元素放到删除结点的位置,然后向下调整
    向上调整大顶堆的算法
        void AdjustUp(int arr[], int n){
            int temp = arr[n], i = n / 2;
            while(i > 0 && arr[i] < temp){
                arr[n] = arr[i];
                n = i;
                i = n / 2;
            }
            arr[n] = temp;
        }

排序算法的性能
    空间复杂度:O(1)
    时间复杂度:
        每次调整的算法的时间复杂度为 O(完全二叉树的高度),即O(log₂n)
        HeapSort算法时间复杂度O(n-1)*(log₂n)即O(nlog₂n)

堆排序是不稳定的

归并排序

归并排序的思想是将多个有序表合并成一个新的有序表
将待排序表长为n的表,看成是n个有序的子表,两两归并,重复此操作,直达合并成一个长度为n的有序表,称为2路归并排序

合并相邻的两个有序表

int tempArr[n];

void Merge(int arr[], int start, int temp, int end) {
	// 两个子表 (start...temp) 和 (temp+1...end)
	int i = start, j = temp + 1, k;
	for(k = start; k <= end; k++) { // 将arr中的数据复制到tempArr中
		tempArr[k] = arr[k];
	}
	
	k = start;
	
	while(i <= temp && j <= end) { // 左表和右表都没有超出范围,将较小的值重新放到arr中
		if(tempArr[i] <= tempArr[j]){
			arr[k++] = tempArr[i++];
		}else{
			arr[k++] = tempArr[j++];
		}
	}
	
	while(i <= temp) {
		arr[k++] = tempArr[i++];
	}
	
	while(j <= end) {
		arr[k++] = tempArr[j++];
	}
	
}

void Sort(int arr[], int start, int end){
	void Merge(int arr[], int start, int temp, int end);
	if(start < end) {
		int temp = (start + end) / 2;
		Sort(arr, start, temp);
		Sort(arr, temp + 1, end);
		Merge(arr, start, temp ,end);
	}	
}

算法效率分析
    空间复杂度:由于Merge操作需要tempArr,所以空间复杂度为O(n)
    时间复杂度:
        第k趟,一共有 n/2^k 对个子序列,这些子序列都要调用Merge进行合并
        要合并的两个子序列共有 2^k 个元素,而merge算法的时间复杂度为O(元素的个数)
        所以第k趟,需要执行 (n/2^k)*(2^k) 次,也就是n次
        当 2^k = n,可以求得一共执行的趟数为 k = log₂n
        而每一趟需要执行 n 次,所以时间复杂度为 O(nlog₂n)

基数排序

基数排序的思想是多关键字排序,有两种实现方法

    最高位优先(MSD):按照最高位排成若干子序列,然后在子序列中按照次高位排序,最终使序列整体有序

    最低位优先(LSD):不需要分成子序列,不通过比较,而是通过分配和收集,过程如下
        原始序列 278 109 063 930 589 184 505 269 008 083
        每一位数都是由 0~9 的数字组成,所以准备10个桶,这些桶都是队列
        从左到右,按照个位数的大小顺序,将关键字扔到桶中,这就是分配,然后从桶底收集这些关键字
        结果序列为 930 063 083 184 505 278 008 109 589 269,然后针对十位和百位重复上述步骤
        算法效率
            空间复杂度:O(r) r为桶队列的个数
            时间复杂度:进行d趟分配和收集,每一趟分配需要O(n)收集需要O(r),所以时间复杂度为O(d(n+r))
        基数排序与序列的初始状态无关
        基数排序是稳定的

基数排序适合关键字很多,并且关键字的取值范围小也就是桶的个数少

如果桶的个数太多了,可以使用最高位优先法,将序列分割成几个子序列,然后对这些子序列进行直插

各种内部排序算法的复杂度总结表

数据结构(c语言描述)

外部排序

基本概念:对大文件排序,排序时将数据一部分一部分的读取到内存中,需要多次进行内存和外存中的交换

在外部排序中时间代价主要考虑磁盘I/O次数

外部排序的基本流程
    将n个记录分成m个规模较小的记录段,这些记录段一般足够小,可以直接读入内存进行排序
    将k个记录段作为一组,一共就是m/k组,取其中一组,将每个记录段的的首位记录读取到内存中
    使用某种算法选出这些记录的最小值,将最小值写回外存,并且将此记录段的次位记录读取到内存
    重复上述步骤,就会得到m/k个较长的记录段,然后再重复上述步骤,完成k路归并算法

置换选择排序

置换选择排序作用:
    此算法用来构造初始归并段,也就是那m个规模较小的有序记录段
    如果这些初始划分的归并段还是太大不能读入内存,则无法在内存中生成初始归并段
    置换选择排序就是用来对这些记录段进行排序从而来构造初始归并段的算法

置换选择排序操作流程:
    将外存记录读入到缓冲区,直到充满缓冲区
    将缓冲区中最小的记录写回外存,作为当前初始归并段的一部分,读取外存记录下一位记录替换空缺位置  
    如果选取的最小记录比初始归并段最大的记录小,则选取次小记录
    重复上述步骤,直到缓冲区中的所有记录都比初始归并段最大的记录小,就生成了一个初始归并段
    重复上述步骤,完成剩余初始归并段的生成

通过置换选择排序得到的m个初始归并段长度都是不同的

最佳归并树

最佳归并树作用:
    将上面得到的m个初始归并段,k个初始归并段作为一组,进行一趟归并,重复此步骤,直到完成最后的排序
    其中,每趟归并,每个记录都会执行两次I/O,可以通过减少归并的趟数,提高效率
    最佳归并树就是为了得到最小I/O而设计的

构造最佳归并树:
    归并的过程可以使用树来描述,树中的每一个结点的权代表的是当前归并段的长度
    归并操作I/O次数=树带权路径长度*2
    为了使归并操作I/O最小,必须树带权路径长度最小,也就是构建k叉哈夫曼树

败者树

败者树的作用
    上面的归并操作,需要选取每一组k个记录段中的最值,败者树就是用来提高选取最值的效率

败者树的构建
    如果不使用败者树,k个值需要做k-1此比较才能得出最值,而败者树只需要log₂k次即可

    败者树有两种结点
        叶子结点:从归并段中读取的段首记录,叶子结点的个数为k
        非叶子结点:度为2,值为段首记录胜者所在的归并段序号

    建立败者树
        将任意两个叶子结点作为一组来建立二叉树,多余的单个结点放在下一轮处理
        将这两个叶子结点的值做pk,大的所在归并段序号作为两者的父结点,小的所在归并段序号作为父结点的父节点
        将上面构建的二叉树根结点p指示的序号的段首值和剩余的单个结点做pk,单个结点作为p的孩子,pk胜的作为p的父结点
        将两个二叉树的根结点指示的序号的段首值做pk,pk胜的作为树的根结点

    调整败者树
        败者树的根结点就是最小值记录的序号,根据此序号去对应归并段中找首值,写回外存
        删除败者树根结点和对应的叶子结点,用归并段下一个值取代叶子结点
        和相邻叶子结点pk,重复建立败者树的步骤

算法性能分析
    时间复杂度
        m个初始归并段的k路归并,归并的趟数为log以k为底m的对数,并且向上取整。n = k^(h-1)其中h-1就是归并的趟数
        每一次归并、置换-选择排序,每个记录都要2次I/O
        k路归并败者树的高度h'=(h-2)+1<=log₂k+1,所以h'为log₂k+1向上取整
        从k路归并败者树中取最值,需要log₂k向上取整次比较,时间复杂度为O(log₂k)
        建立k路归并败者树,时间复杂度为O(klog₂k)
    空间复杂度 O(1)

相关文章:

  • 2021-12-15
  • 2022-12-23
  • 2021-07-23
  • 2021-07-31
  • 2021-09-09
  • 2021-11-28
  • 2021-05-26
  • 2021-08-15
猜你喜欢
  • 2022-12-23
  • 2021-05-24
  • 2021-06-30
  • 2021-09-23
  • 2021-12-07
  • 2021-12-15
  • 2021-10-06
相关资源
相似解决方案