数据结构
1 绪论
1.1.1 数据结构的基本概念
--数据元素,数据项:
数据元素是数据的基本单位,通常作为一个整体进行考虑。
一个数据元素可以有若干个数据项组成,数据项是构成数据元素的不可分割的最小单位。
--数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
--数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
1.1.2 数据结构的三要素
--三要素:
逻辑结构:
集合结构
线性结构 -> 一对一
树形结构 -> 一对多
图状结构 -> 多对多
数据的运算:结合逻辑结构,实际需求来定义基本运算
物理结构(存储结构)
--线性结构:
除了第一个元素,所有元素都有唯一前驱
除了第一个元素,所有元素都有唯一后继
--数据的存储结构:
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中。
链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示存储地址的指针来表示元素之间的逻辑关系。
索引存储:存储元素信息的同时,建立附加的索引表。表中每项称为索引项,一般形式是(关键字,地址)。
散列存储(哈希存储):根据元素的关键字直接计算出存储地址。
1.2.1 算法的基本概念
--算法是对特定问题求解步骤的一种描述 ,它是指令的有线序列,其中每条指令表示一个或多个操作。
--算法的特性:
有穷性:算法必须在执行有穷步后结束,且每一步都可以在有穷时间内完成。
确定性:算法中每条指令必须有明确的含义,对于相同的输入只能得到相同的输出。
可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
--“好”算法的特质:
正确性:能正确地解决求解问题。
可读性:具有良好的可读性,以帮助人们理解
健壮性:输入非法数据是,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
高效率与低存储量需求。
1.2.2 算法的时间复杂度
--O(1) < O(log2 n) < O(n) < O(n*log2 n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
1.2.3 算法的空间复杂度
--O(1) < O(log2 n) < O(n) < O(n*log2 n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
2 线性表
2.1.1 顺序表的定义
优点:可随机存取,存储密度高
缺点:要求大片连续空间,改变容量不方便
静态分配
#include <stdio.h> #define MaxSize 10 //定义最大长度 typedef struct{ ElementType data[MaxSize]; //用静态的数组存放数据元素 int length; }SqList; //初始化一个顺序表 void InitList(SqList &L){ for(int i=0; i<MaxSize; i++){ L.data[i]=0; //所有的数据元素设置为默认值 } L.length=0; //初始长度为0 } int main(){ SqList L; InitList(L); //...... return 0; }
动态分配
关键在于动态申请和释放内存空间(使用malloc,free函数)
#include <stdio.h> #define InitSize 10 //顺序表的初始长度 typedef struct{ ElementType *data; //指示动态分配数组的指针,指向第一个元素的地址 int MaxSize; //顺序表的最大容量 int length; }SqList; //初始化一个顺序表 void InitList(SqList &L){ //用malloc函数申请一片连续的存储空间 L.data = (ElementType*)malloc(InitSize*sizeof(int)); //最后转化成 ElementType* 类型 L.length=0; //初始长度为0 L.MaxSize=InitSize; } //增加动态数组的长度 void IncreaseSize(SqList &L, int len){ ElementType *p = L.data; L.data = (ElementType*)malloc( (L.MaxSize+len) * sizeof(int) ); for(int i=0; i < L.length; i++){ L.data[i] = p[i]; //将数据复制到新的区域 } L.MaxSize=L.MaxSize+len; //长度改变 free(p); //释放原来的内存 } int main(){ SqList L; InitList(L); //...... IncreaseSize(L,5); return 0; }
2.1.2 顺序表的插入和删除
插入
时间复杂度:O(n)
#include <stdio.h> #define InitSize 10 //顺序表的初始长度 typedef struct{ ElementType *data; //指示动态分配数组的指针,指向第一个元素的地址 int MaxSize; //顺序表的最大容量 int length; }SqList; bool ListInsert(SqList &L, int i, int e){ if(i<1 || i>L.length+1) //判断i的范围是否有效 return false; if(L.length >= MaxSize) //当前存储空间已满,不能插入 return false; for(int j=L.length; j>=i; j--){ L.data[j] = L.data[j-1]; //将第i个元素以及之后的元素向后移动一个单位 } L.data[i-1] = e; L.length++; return true; }
删除
时间复杂度:O(n)
#include <stdio.h> #define InitSize 10 //顺序表的初始长度 typedef struct{ ElementType *data; //指示动态分配数组的指针,指向第一个元素的地址 int MaxSize; //顺序表的最大容量 int length; }SqList; bool ListDelete(SqList &L, int i, int &e){ if(i<1 || i>L.length+1) //判断i的范围是否有效 return false; e = L.data[i-1]; //将被删除的元素赋值给e for(int j=i; j<L.length; j++){ L.data[j-1] = L.data[j]; //将第i个元素以及之后的元素向前移动一个单位 } L.length--; return true; }
2.1.3 顺序表的查找
按位查找
时间复杂度:O(1)
#include <stdio.h> #define InitSize 10 //顺序表的初始长度 typedef struct{ ElementType *data; //指示动态分配数组的指针,指向第一个元素的地址 int MaxSize; //顺序表的最大容量 int length; }SqList; ElementType GetElement(SqList L, int i){ return L.data[i-1]; }
按值查找
时间复杂度:O(n)
#include <stdio.h> #define InitSize 10 //顺序表的初始长度 typedef struct{ ElementType *data; //指示动态分配数组的指针,指向第一个元素的地址 int MaxSize; //顺序表的最大容量 int length; }SqList; int LocateElement(SqList L, ElementType e){ for(int i=0; i<L.length; i++){ if(L.data[i]==e){ //基本数据类型可以用 == 比较,否则需要用其他方法 return i+1; } } return 0; }
2.2.1 单链表的定义
优点:不要求到连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针
typedef sturut LNode{ //定义单链表结点类型 ElementType data; //数据元素 struct LNode *next; //指针指向下一个结点 }LNode, *LinkList; //LNode为结点的别名,LinkList为结点的指针的别名 //使用LinkList --强调这是一个单链表 //使用LNode * --强调这是一个结点 //增加一个新的结点 sturut LNode *p = (sturut LNode *)malloc(sizeof(sturut LNode)); //申请一个结点所需空间,并用指针p指向这个结点
不带头结点的单链表
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; //初始化一个空的单链表 bool InitList(LinkList &L){ L = NULL; //空表,暂时没有任何结点 return true; } //判断链表是否为空 bool Empty(LinkList L){ return (L==NULL); } void test(){ LinkList L; //声明一个指向单链表的指针 --防止脏数据 InitList (L); //初始化一个空表 //...... }
带头结点的单链表
头结点不存储数据,只是为了实现某些操作更方便
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; //初始化一个空的单链表(带头结点) bool InitList(LinkList &L){ L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点 if(L==NULL){ //内存不足,分配失败 return false; } L->next = NULL; //头结点之后暂时没有结点 return true; } void test(){ LinkList L; //声明一个指向单链表的指针 --防止脏数据 InitList (L); //初始化一个空表 //...... }
2.2.2 单链表的插入删除
按位序插入(带头结点)
在第i个位置插入指定的元素e
时间复杂度:O(n)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; bool ListInsert(LinkList &L, int i, ElementType e){ if(i<1) return false; LNode *p; //指针p指向当前扫描到的结点 int j = 0; //当前p指的是第几个结点 p = L; //L指向头结点,头结点看做第0个结点(不存储数据) while(p != NULL && j<i-1){ //循环找到第i-1个结点 p=p->next; j++; } if(p==NULL) return false; //i值不合法 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
按位序插入(不带头结点)
在第i个位置插入指定的元素e
当i为1时,需要改动头指针
时间复杂度:O(n)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; bool ListInsert(LinkList &L, int i, ElementType e){ if(i<1) return false; if(i==1){ //i=1的情况与其他情况不同 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; return true; } LNode *p; //指针p指向当前扫描到的结点 int j = 0; //当前p指的是第几个结点 p = L; //L指向第一个结点,不是头结点 while(p != NULL && j<i-1){ //循环找到第i-1个结点 p=p->next; j++; } if(p==NULL) return false; //i值不合法 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
指定结点的后插操作
在p结点之后插入指定的元素e
时间复杂度:O(1)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; bool InsertNextNode(LNode *p,ElementType e){ if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s==NULL) //内存分配失败 return false; s->data = e; s->next = p->next; p->next = s; return true; }
指定结点的前插操作
在p结点之前插入指定的元素e
时间复杂度:O(1)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; bool InsertNextNode(LNode *p,ElementType e){ if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s==NULL) //内存分配失败 return false; s->next = p->next; p->next = s; //新结点 s 连到 p 之后 s->data = p->data; //p 中的元素复制到s里 p->data = e; //p中的元素覆盖为e return true; }
按位序删除(带头结点)
删除表中第 i 个位置的元素,并用e返回删除的元素的值
时间复杂度:O(n)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; bool ListDelete(LinkList &L, int i, ElementType e){ if(i<1) return false; LNode *p; //指针p指向当前扫描到的结点 int j = 0; //当前p指的是第几个结点 p = L; //L指向头结点,头结点看做第0个结点(不存储数据) while(p != NULL && j<i-1){ //循环找到第i-1个结点 p=p->next; j++; } if(p==NULL) return false; //i值不合法 if(p->next == NULL) return false; //第 i-1 个结点之后没有其他结点 LNode *q = p->next; //令 q 指向被删除的结点 p->next = q->next; //将 q 结点抛出 e = q->data; //用 e 返回元素的值 free(q); //释放 q 的存储空间 return true; }
按位序删除(不带头结点)
删除表中第 i 个位置的元素,并用e返回删除的元素的值
当i为1时,需要改动头指针
时间复杂度:O(n)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; bool ListDelete(LinkList &L, int i, ElementType e){ if(i<1) return false; LNode *p; //指针p指向当前扫描到的结点 int j = 0; //当前p指的是第几个结点 p = L; //L指向第一个结点,不是头结点 if(i==1){ //i=1的情况与其他情况不同 e = L->data; //用 e 返回元素的值 L = L->next; free(p); return true; } while(p != NULL && j<i-1){ //循环找到第i-1个结点 p=p->next; j++; } if(p==NULL) return false; //i值不合法 if(p->next == NULL) return false; //第 i-1 个结点之后没有其他结点 LNode *q = p->next; //令 q 指向被删除的结点 p->next = q->next; //将 q 结点抛出 e = q->data; //用 e 返回元素的值 free(q); //释放 q 的存储空间 return true; }
指定结点的删除
删除指定的结点 p
时间复杂度:O(1)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; bool DeleteNode(LNode *p){ if(p==NULL) return false; LNode *q = p->next; //令 q 指向 p 的后继结点 p->data = p->next->data; //和后继结点交换数据域 p->next = q->next; //将 q 结点从链中断开 free(q); return true; } //当 p 是最后一个结点,p->next为null,代码出现bug,只能从头找到 p 的前驱结点,时间复杂度变为O(n )
2.2.3 单链表的查找
按位查找(带头结点)
获取表中第 i 个结点
时间复杂度:O(n)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; LNode * GetElement(LinkList L, int i){ if(i<0) return NULL; LNode *p; int j = 0; p = L; while(p != NULL && j<i){ p = p->next; j++; } return p; }
按值查找(带头结点)
在表中查找具有给定值的元素
时间复杂度:O(n)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; LNode * LocateElement(LinkList L, ElementType e){ LNode *p = L->next; //从第一个结点开始查找数据为 e 的结点 while(p != NULL && p->data != e){ p = p->next; } return p; //找到后返回 p ,否则返回NULL }
求表的长度(带头结点)
时间复杂度:O(n)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; int Length(LinkList L){ int len = 0; LNode *p = L; while(p->next != NULL){ p = p->next; len++; } return len; //找到后返回 p ,否则返回NULL }
2.2.4 单链表的建立
尾插法(带头结点)
时间复杂度:O(1)
对尾结点的后插操作
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; LinkList List_TailInsert(LinkList &L){ int x; //设数据类型为int L=(LinkList)malloc(sizeof(LNode)); //建立头结点 LNode *s,*r =L; //r 为表尾指针 scanf("%d",&x); while(x!=9999){ //输入9999表示结束 s=(LNode *)malloc(sizeof(LNode)); s->data = x; r->next = s; r = s; //指向新的表尾结点 scanf("%d",&x); } r->next = NULL; //尾结点指针置空 return L; }
头插法(带头结点)
对头结点的后插操作
时间复杂度:O(1)
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LinkList; LinkList List_HeadInsert(LinkList &L){ LNode *s; int x; //设数据类型为int L=(LinkList)malloc(sizeof(LNode)); //建立头结点 L->next = NULL; //初始为空结点 //不加可能会有野指针问题,指向脏数据 scanf("%d",&x); //输入结点的值 while(x!=9999){ //输入9999表示结束 s=(LNode *)malloc(sizeof(LNode)); s->data = x; s->next = L->next; L->next = s; //将新结点插入表中。L为头指针 scanf("%d",&x); } return L; }
2.3.1 双链表的初始化(带头结点)
typedef sturut DNode{ ElementType data; struct DNode *prior,*next; }DNode, *DLinkList; bool InitDLinkList(DLinkList &L){ L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点 if(L == NULL) //内存不足,分配失败 return false; L->prior = NULL; //头结点的 prior 永远指向NULL L->next = NULL; return true; }
2.3.2 双链表的插入(带头结点)
将结点 *s 插入到结点 *p 之后
typedef sturut DNode{ ElementType data; struct DNode *prior,*next; }DNode, *DLinkList; bool InsertNextDNode(DNode *p,DNode *s){ if(p==NULL || s==NULL) return false; s->next = p->next; if(p->next !=NULL) //如果 p 结点有后继结点 p->next->prior = s; s-> prior = p; p->next = s; return true; }
2.3.3 双链表的删除(带头结点)
将结点 *p 的后继结点 *q 删除
typedef sturut DNode{ ElementType data; struct DNode *prior,*next; }DNode, *DLinkList; bool DeleteNextDNode(DNode *p){ if(p==NULL) return false; DNote *q = p->next; //p 的后继结点 if(q==NULL) //p 没有后继 return false; p->next = q->next; if(q->next !=NULL) //q 结点不是最后一个结点 q->next->prior = p; free(q); return true; }
2.4.1 循环单链表的初始化
typedef sturut LNode{ ElementType data; struct LNode *next; }LNode, *LLinkList; bool InitList(LinkList &L){ L = (LNode*)malloc(sizeof(LNode)); //分配一个头结点 if(L == NULL) //内存不足,分配失败 return false; L->next = L; //头结点的 next 指向头结点 return true; } //判断循环单链表是否为空 bool Empty(LinkList L){ return (L->next == L); } //判断一个结点是否为循环单链表的尾结点 bool isTail(LinkList L, LNode *p){ return (p->next == L); }
2.4.2 循环双链表的初始化
typedef sturut DNode{ ElementType data; struct DNode *prior,*next; }DNode, *DLinkList; bool InitDLinkList(DLinkList &L){ L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点 if(L == NULL) //内存不足,分配失败 return false; L->prior = L; //头结点的 prior 指向头结点 L->next = L; //头结点的 next 指向头结点 return true; } //判断循环单链表是否为空 bool Empty(LinkList L){ return (L->next == L); } //判断一个结点是否为循环单链表的尾结点 bool isTail(LinkList L, LNode *p){ return (p->next == L); }
2.5.1 静态链表
静态链表:分配一整片连续的内存空间,各个结点集中安置
数据以类似于数组的方式储存,0号结点充当“头结点”,游标充当“指针”,游标为-1代表已经到达表尾。
游标并不是结点的地址,而是类似于在这一片空间中的偏移量。
游标占用 4B
优点:增,删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始遍历;容量固定不可变
2.5.2 静态链表的建立
下面两段代码等价
#define MaxSize 10 struct Node{ ElementType data; int next; //下一个元素的数组下标 }; void testSLinkList(){ struct Node a[MaxSize]; //数组a作为静态链表 //但这样命名感觉像 Node 型的数组,所以有下面的代码 }
#define MaxSize 10 typedef struct{ ElementType data; int next; } SLinkList[MaxSize]; //把结构体重命名为SLinkList //SLinkList[MaxSize] 是个数组 void testSLinkList(){ SLinkList a; //数组a作为静态链表 //这样命名就体现出 a 是一个链表 }
3 栈和队列
3.1.1 栈的顺序存储
初始化
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; //静态数组存放栈内元素 int top; //栈顶指针 } SqStack; //初始化栈 void InitStack(SqStack &S){ S.top = -1; //初始化栈顶指针 } //判断栈空 void StackEmpty(SqStack &S){ return (S.top == -1); //初始化栈顶指针 } void testStack(SqStack &S){ SqStack S; //声明一个顺序栈(分配内存空间) InitStack(S); //初始化 }
进栈
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; //静态数组存放栈内元素 int top; //栈顶指针 } SqStack; bool Push(SqStack &S, ElementType x){ if(s.top == MaxSize-1) //栈满,报错 return false; s.top = s.top + 1; s.data[s.top] = x; //进栈 return true; }
栈
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; //静态数组存放栈内元素 int top; //栈顶指针 } SqStack; bool Pop(SqStack &S, ElementType &x){ if(s.top == -1) //栈空,报错 return false; x = s.data[s.top]; //栈顶元素出栈 s.top = s.top - 1; return true; }
读栈顶元素
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; //静态数组存放栈内元素 int top; //栈顶指针 } SqStack; bool GetTop(SqStack &S, ElementType &x){ if(s.top == -1) //栈空,报错 return false; x = s.data[s.top]; //x 记录栈顶元素 return true; }
共享栈
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; //静态数组存放栈内元素 int top0; //0号栈栈顶指针 int top1; //1号栈栈顶指针 } ShStack; //初始化栈 void InitStack(SqStack &S){ S.top0 = -1; //初始化栈顶指针 S.top1 = MaxSize; //初始化栈顶指针 } //判断栈满 void StackEmpty(SqStack &S){ return ((S.top0 + 1) == S.top1); //初始化栈顶指针 } void testStack(SqStack &S){ SqStack S; //声明一个顺序栈(分配内存空间) InitStack(S); //初始化 }
3.2.1 队列的顺序存储
初始化
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; //静态数组存放队列元素 int front,rear; //队头指针和队尾指针 } SqQueue; //初始化队列 void InitQueue(SqQueue &Q){ Q.rear = Q.front = 0; //初始时,队头、队尾指针指向0 } void testStack(SqStack &Q){ SqQueue Q; //声明一个队列(分配内存空间) InitQueue(Q); //初始化 }
入队
bool EnQueue(SqQueue &Q, ElementType x){ if( (Q.rear+1)%MaxSize == Q.front ) //队列已满 return false; Q.data[Q.rear] = x; //新元素插入队尾 Q.rear = (Q.rear + 1) % MaxSize; //队尾指针+1取模 return true; }
出队
bool DeQueue(SqQueue &Q, ElementType x){ if(Q.rear == Q.front) //队列为空 return false; x = Q.data[Q.front]; Q.front = (Q.front + 1) % MaxSize; //队头指针后移 return true; }
获取队头元素
bool GetTopSqQueue &Q, ElementType x){ if(Q.rear == Q.front) //队列为空 return false; x = Q.data[Q.front]; return true; }
判断队列已满/为空
方法1:
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; int front,rear; } SqQueue; //判断队列空 void StackEmpty(SqStack Q){ return (Q.rear == Q.front); } //判断队列满 void StackFull(SqStack Q){ return ((Q.rear+1)%MaxSize == Q.front); }
方法2:
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; int front,rear; int size; //队列当前长度 入队size++,出队size-- } SqQueue; //判断队列空 void StackEmpty(SqStack Q){ return (size == MaxSize); } //判断队列满 void StackFull(SqStack Q){ return (size == 0); }
方法3:
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; int front,rear; int tag; //标记上一次执行的是 入队 or 出队 操作,入队为 1 ,出队为 0 初始化为0 } SqQueue; //判断队列空 void StackEmpty(SqStack Q){ return (Q.rear == Q.front && tag == 0); //队头指针和队尾指针相同,上一次操作为出队 } //判断队列满 void StackFull(SqStack Q){ return (Q.rear == Q.front && tag == 1); //队头指针和队尾指针相同,上一次操作为入队 }
3.2.2 队列的链式存储
初始化(带头结点)
#define MaxSize 10 typedef struct LinkNode{ ElementType data; struct LinkNode *next; }LinkNode; typedef struct{ LinkNode *front, *rear; }LinkQueue; //初始化队列 void InitQueue(LinkQueue &Q){ Q.rear = Q.front = (LinkNode *)malloc(sizeof(LinkNode)); //初始时,队头、队尾指针指向头结点 Q.front->next = NULL; } void test(LinkQueue &Q){ LinkQueue Q; //声明一个队列 InitQueue(Q); //初始化 } //是否为空 bool IsEmpty(LinkQueue Q){ return (Q.front == Q.rear); }
初始化(不带头结点)
#define MaxSize 10 typedef struct LinkNode{ ElementType data; struct LinkNode *next; }LinkNode; typedef struct{ LinkNode *front, *rear; }LinkQueue; //初始化队列 void InitQueue(LinkQueue &Q){ Q.front = null; Q.rear = null; } //是否为空 bool IsEmpty(LinkQueue Q){ return (Q.front == null); }
入队(带头结点)
void EnQueue(LinkQueue &Q, ElementType x){ LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); s->data = x; s->next = null; Q.rear->next = s; Q.rear = s; }
入队(不带头结点)
void EnQueue(LinkQueue &Q, ElementType x){ LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); s->data = x; s->next = null; if(Q.front == null){ //插入的是空队列 Q.front = s; Q.rear = s; }else{ Q.rear->next = s; Q.rear = s; } }
出队(带头结点)
bool DeQueue(LinkQueue &Q, ElementType x){ if(Q.rear == Q.front) //队列为空 return false; LinkQueue *p = Q.front->next; x = p->data; //用x返回队头元素 Q.front->next = p->next; //修改头结点的next指针 if(Q.rear == p) //最后一个结点出队 Q.rear = Q.front; free(p); return true; }
出队(不带头结点)
bool DeQueue(LinkQueue &Q, ElementType x){ if(Q.front == null) //队列为空 return false; LinkQueue *p = Q.front; // p 指向出队的元素 x = p->data; //用x返回队头元素 Q.front = p->next; //修改头结点的next指针 if(Q.rear == p) //最后一个结点出队 Q.rear = null; Q.front = null; free(p); return true; }
队列满的情况
bool IsFull(LinkQueue &Q, ElementType x){ if(Q.rear == Q.front) //队列为空 return false; x = Q.data[Q.front]; return true; }
3.2.3 双端队列
双端队列:两端插入,两端删除
输入受限的双端队列:一端插入,两端删除
输出受限的双端队列:两端插入,一端删除
3.3.1 栈在括号匹配中的应用
#define MaxSize 10 typedef struct{ ElementType data[MaxSize]; //静态数组存放栈内元素 int top; //栈顶指针 } SqStack; bool bracketCheck(char str[], int length){ SqStack S; InitStack(S); for(int i=0; i<length; i++){ if(str[i] == "(" || str[i] == "[" || str[i] == "{"){ //扫描到左括号,入栈 Push(S,str[i]); }else{ if(StackEmpty(S)) //扫描到右括号,但当前栈空 return false; char topElem; Pop(S, topElem); //弹出栈顶的括号 if(str[i] == ")" && topElem != "(") return false; if(str[i] == "]" && topElem != "[") return false; if(str[i] == "}" && topElem != "{") return false; } } return StackEmpty(S); }
4 串
4.1 模式匹配
模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置
4.2 朴素模式匹配算法
将主串中所有长度符合需求的子串依次与模式串对比,直到找到完全匹配的子串,或所有的子串都不匹配。
字符串方法:
int Index(SString S, SString T){ int i = 1, n = StrLength(S), m = StrLength(T); SString sub; //用来暂存子串 while(i < n-m+1){ SubString(sub,S,i,m); if(StrCompare(Sub, T) != 0) ++i; else return i; } return 0; //不存在匹配的子串 }
数组下标方法:
最坏时间复杂度:O(nm)
int Index(SString S, SString T){ int i = 1, j = 1; while(i < S.length && j <= T.length){ if(S.ch[i] == T.ch[j]){ ++i; ++j; } else{ i = i-j+2; j = 1; } } if(j > T.length) return i-T.length; else return 0; }
4.3.1 KMP算法
int Index_KMP(SString S, SString T, int next[]){ int i = 1, j = 1; while(i < S.length && j <= T.length){ if(j == 0 || S.ch[i] == T.ch[j]){ ++i; ++j; } else{ j = next[j]; //模式串向右移动 } } if(j > T.length) return i-T.length; //匹配成功 else return 0; }
串的前缀:包含第一个字符,且不包含最后一个字符的子串
串的后缀:包含最后一个字符,且不包含第一个字符的子串
next数组:当第 j 个字符匹配失败,由前1~j-1 个字符组成的串记为 S, 则 next[j] = S的最长相等前后缀的长度+1
特别的:next[1] = 0, next[2] = 1
4.3.2 求next数组
void get_next(SString T, int next[]){ int i=1,j=0; next[1] = 0; while(i < T.length){ if(j == 0 || T.ch[i] == T.ch[j]){ ++i; ++j; //若pi = pj,则 next[j+1]=next[j]+1 next[i] = j; }else{ //否则 j=next[j],循环结束 j = next[j]; } } }
4.3.3 求nextval数组
void get_nextval(SString T, int nextval[]){ int i=1,j=0; nextval[1] = 0; while(i < T.length){ if(j == 0 || T.ch[i] == T.ch[j]){ ++i; ++j; if(T.ch[i] != T.ch[j]){ nextval[i] = j; }else{ nextval[i] = nextval[j]; } }else{ //否则 j=next[j],循环结束 j = nextval[j]; } } }
5 树与二叉树
5.1.1 二叉树的定义和基本术语
满二叉树:高度为h,且含有2^h-1个结点的二叉树
特点:
1,只有最后一层有叶子结点
2,不存在度为1的结点
3,按层序从1开始编号,结点 i 的左孩子为 2i ,右孩子为 2i+1 ,父结点为 i/2 (向下取整)
完全二叉树:满二叉树中最后n个结点缺失
特点:
1,只有最后两层可能有叶子结点
2,最多只有一个度为1的结点
3,按层序从1开始编号,结点 i 的左孩子为 2i ,右孩子为 2i+1 ,父结点为 i/2 (向下取整)
4,i <= i/2 (向下取整) 为分支结点,i > i/2 (向下取整) 为叶子结点
平衡二叉树:树上任一结点的左子树和右子树深度之差不超过1
5.1.2 二叉树的性质
~具有 n 个结点的完全二叉树的高度 h 为 log(n+1)(向上取整) 或 logn +1(向下取整)
5.1.3 二叉树的存储结构
顺序存储:只适合存储完全二叉树
#define MaxSize 100 struct TreeNode{ ElementType value; bool inEmpty; }; TreeNode t[MaxSize]; for(int i=0; i<MaxSize; i++){ t[i].isEmpty = true; }
链式存储:
typedef struct BiTNode{ ElementType data; struct BiTNode *lchild,* rchild; }BiTNode,* BiTree; BiTree root = null; //定义一颗空树 root 指向null //插入根结点 root = (BiTree) malloc(sizeof(BiTNode)); root->data = {1}; root->lchild = null; root->rchild = null; //插入新结点 BiTNode *p = (BiTNode *) malloc(sizeof(BiTNode)); p->data = {2}; p->lchild = null; p->rchild = null; root->lchild = p; //作为根节点的左孩子
三叉链表:需要向上遍历时,在结点中加入父结点指针
typedef struct BiTNode{ ElementType data; struct BiTNode *lchild,* rchild; struct BiTNode *parent; //父结点指针 }BiTNode,* BiTree;
5.2.1 二叉树的先中后序遍历
先序遍历:
1,若二叉树为空,则什么也不做
2,若二叉树非空:
①访问根节点
②先序遍历左子树
③先序遍历右子树
typedef struct BiTNode{ ElementType data; struct BiTNode *lchild,* rchild; }BiTNode,* BiTree; //先序遍历 void PreOrder(BiTree T){ if(T!=null){ visit(T); //访问根节点 PreOrder(T->lchild); //递归遍历左子树 PreOrder(T->rchild); //递归遍历右子树 } }
中序遍历:
1,若二叉树为空,则什么也不做
2,若二叉树非空:
①中序遍历左子树
②访问根节点
③中序遍历右子树
typedef struct BiTNode{ ElementType data; struct BiTNode *lchild,* rchild; }BiTNode,* BiTree; //中序遍历 void InOrder(BiTree T){ if(T!=null){ PreOrder(T->lchild); //递归遍历左子树 visit(T); //访问根节点 PreOrder(T->rchild); //递归遍历右子树 } }
后序遍历:
1,若二叉树为空,则什么也不做
2,若二叉树非空:
①后序遍历左子树
②后序遍历右子树
③访问根节点
typedef struct BiTNode{ ElementType data; struct BiTNode *lchild,* rchild; }BiTNode,* BiTree; //后序遍历 void PostOrder(BiTree T){ if(T!=null){ PostOrder(T->lchild); //递归遍历左子树 PostOrder(T->rchild); //递归遍历右子树 visit(T); //访问根节点 } }
5.2.2 二叉树的层序遍历
1,初始化一个辅助队列
2,根结点入队
3,若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
4,重复3直至队列为空
typedef struct BiTNode{ ElementType data; struct BiTNode *lchild,* rchild; }BiTNode,* BiTree; //链式队列结点 typedef struct LinkNode{ BiTNode *data; //存指针而不是结点 struct LinkNode *next; }LinkNode; // typedef struct LinkNode{ LinkNode *front, *rear; //队头队尾 }LinkQueue; //层序遍历 void LevelOrder(BiTree T){ LinkQueue Q; InitQueue(Q); BiTree p; Enqueue(Q,T); //根节点入队 while(!IsEmpty(Q)){ DeQueue(Q,p); //队头结点出队 visit(p); if(p->lchild != null) Enqueue(Q,p->lchild); //左孩子入队 if(p->rchild != null) Enqueue(Q,p->rchild); //右孩子入队 } }
5.2.3 线索二叉树
typedef struct ThreadTNode{ ElementType data; struct ThreadTNode *lchild,* rchild; int ltag, rtag; //左右线索标志:tag=0,指针指向孩子,tag=1,指针是“线索” }ThreadTNode,* ThreadTree;
5.2.4 二叉树线索化
中序线索化:
typedef struct ThreadTNode{ ElementType data; struct ThreadTNode *lchild,* rchild; int ltag, rtag; }ThreadTNode,* ThreadTree; //全局变量,指向当前访问结点的前驱 ThreadNode *pre = null; //中序遍历 void InOrder(BiTree T){ if(T!=null){ PreOrder(T->lchild); //递归遍历左子树 visit(T); //访问根节点 PreOrder(T->rchild); //递归遍历右子树 } } void visit(ThreadTNode *q){ if(q->lchild == null){//左子树为空,建立前驱线索 q->lchild = pre; q->ltag = 1; } if(pre != null && pre->rchild == null){//左子树为空,建立前驱线索 pre->rchild = q; //建立前驱结点的后继线索 pre->rtag = 1; } pre = q; //pre指针前移 } //中序线索化二叉树 void CreateInThread(ThreadTree T){ pre = null; if(T != null){ //非空二叉树才能线索化 InOrder(T); if(pre->rchild == null) pre->rtag = 1; } }
先序线索化:
typedef struct ThreadTNode{ ElementType data; struct ThreadTNode *lchild,* rchild; int ltag, rtag; }ThreadTNode,* ThreadTree; ThreadNode *pre = null; //先序遍历 void PreOrder(BiTree T){ if(T!=null){ visit(T); if(T->ltag == 0) //lchild不是前驱线索 PreOrder(T->lchild); PreOrder(T->rchild); } } void visit(ThreadTNode *q){ if(q->lchild == null){ q->lchild = pre; q->ltag = 1; } if(pre != null && pre->rchild == null){ pre->rchild = q; pre->rtag = 1; } pre = q; } void CreatePreThread(ThreadTree T){ pre = null; if(T != null){ PreOrder(T); if(pre->rchild == null) pre->rtag = 1; } }
5.2.5 线索二叉树中找前驱后继
中序线索二叉树找后继:
//找到以 p 为根的子树中,第一个被中序遍历的结点 ThreadNode *Fristnode(ThreadNode *p){ while(p->ltag = 0) p = p->lchild; return p; } //在中序线索二叉树中找到 p 的后继结点 ThreadNode *Nextnode(ThreadNode *p){ //rtag=0,后继为右子树中最左下结点,rtag=1,后继为右子结点 if(p->rtag == 0) return Fristnode(p->rchild); //右子树中最左下结点 else return p->rchild; } //对中序线索二叉树进行中序遍历(利用线索实现的非递归算法) void Inorder(ThreadNode *T){ for(ThreadNode *p = Fristnode(T); p != null; p = Nextnode(p)) visit(p); }
中序线索二叉树找前驱:
//找到以 p 为根的子树中,最后一个被中序遍历的结点 ThreadNode *Lastnode(ThreadNode *p){ while(p->ltag = 0) p = p->rchild; return p; } //在中序线索二叉树中找到 p 的前驱结点 ThreadNode *Prenode(ThreadNode *p){ //rtag=0,后继为左子树中最右下结点,rtag=1,前驱为左子结点 if(p->ltag == 0) return Lastnode(p->lchild); //左子树中最右下结点 else return p->lchild; } //对中序线索二叉树进行逆向中序遍历 void RevInorder(ThreadNode *T){ for(ThreadNode *p = Lastnode(T); p != null; p = Prenode(p)) visit(p); }
先序线索二叉树找前驱:
1,如果能找到 p 的父节点,且 p 是左孩子 -> p 的父节点为其前驱
2,如果能找到 p 的父节点,且 p 是右孩子,其左兄弟为空 -> p 的父节点为其前驱
3,如果能找到 p 的父节点,且 p 是右孩子,其左兄弟非空 -> p 的前驱为其左兄弟中最后一个被先序遍历的结点
4,如果 p 是根结点,则 p 没有先序前驱
后序线索二叉树找后继:
1,如果能找到 p 的父节点,且 p 是右孩子 -> p 的父节点为其后继
2,如果能找到 p 的父节点,且 p 是左孩子,其右兄弟为空 -> p 的父节点为其后继
3,如果能找到 p 的父节点,且 p 是左孩子,其右兄弟非空 -> p 的后继为其右兄弟中第一个被后序遍历的结点
4,如果 p 是根结点,则 p 没有后序后继
5.3.1 树的储存结构
双亲表示法(顺序存储):
优点:查指定的双亲很方便
缺点:查指定结点的孩子只能从头遍历
#define MAX_TREE_SIZE 100 //树的结点定义 typedef struct{ ElementType data; int parent; //双亲位置域 }PTNode; //树的类型定义 typedef struct{ PTNode nodes[MAX_TREE_SIZE]; //双亲表示 int n; //结点数 }PTree;
孩子兄弟表示法(链式存储):
typedef struct CSNode{ ElementType data; struct CSNode *firstchild;*nextsibling; //第一个孩子和右兄弟指针 } //二叉树的结点 typedef struct BiTNode{ ElementType data; struct CSNode *lchild;*rchild; }BiTNode,*BiTree;
5.4.1 哈夫曼树
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点的权值的乘积。
树的带权路径长度(WPL):树中所有的叶节点的带权路径长度之和。
在含有 n 个带权叶节点的二叉树中,WPL最小的二叉树称为哈夫曼树,也称最优二叉树。
哈夫曼树的构造:给定 n 个权值分别为w1~wn的结点,构造哈夫曼树的方法如下:
1,将这 n 个结点分别作为 n 颗仅含一个结点的二叉树,构成森林F
2,构造一个新结点,从F中选出两棵根节点权值最小的树作为新结点的左右子树,并将新结点的权值设为左右子树的根节点的权值的和
3,从F中删除刚才选的两棵树,同时将新得到的树加入F
4,重复2和3,直到F中只剩一棵树
特点:
1,每个初始结点最终都称为叶节点,权值越小的结点到根节点的路径越长。
2,哈夫曼树的结点总数为 2n-1。
3,哈夫曼树中不存在度为 1 的点。
4,哈夫曼树不唯一,但WPL必然相同且最小。
5.4.2 并查集
#define SIZE 13 int UFSets[SIZE]; //集合元素数组 //初始化并查集 void Initial(int S[]){ for(int i=0;i < SIZE;i++) S[i] = -1; //每个元素自成一派 } //Find “查”操作,找 x 所属集合(返回 x 所属的根结点) O(n) int Find(int S[],int x){ while(S[x]>=0) //循环寻找x的根 x = S[x]; return x; } //Union “并”操作,将两个集合合并为一个,只需要把一个集合的根节点指向另一个集合的根结点 O(1) void Union(int S[],int Root1, int Root2){ //要求Root1和Root2是不同的集合 if(Root1 == Root2) return ; //将Root2连接到Root1下 S[Root2] = Root1; }
对Union操作进行优化:
根节点的数组元素的绝对值为该树的结点数
//Union 优化-->小树合并到大树 void Union(int S[],int Root1, int Root2){ if(Root1 == Root2) return ; if(Root1 > Root2){ S[Root1] += S[Root2]; //累加结点总数 S[Root2] = Root1; }else{ S[Root2] += S[Root1]; S[Root1] = Root2; } }
5.4.3 并查集的终极优化
对Find操作进行优化:
//Find 优化-->先找到根节点,再“压缩路径” int Find(int S[],int x){ int root = x; while(S[root] >= 0) root = S[root]; //循环找到根 while(x != root){ //压缩路径 int t =S[x]; // t 指向 x 的父结点 S[x] = root; // x 直接挂到根节点下 x = t; } return root; }
6 图
6.1.1 图的基本概念
简单图:
1,不存在重复边
2,不存在顶点到自身的边
多重图:
1,图中某两个顶点之间的边数多于一条,又允许自身到自身
度:
对于无向图,顶点 v 的度是指依附于该顶点的边的条数,记作TD(v)。
对于有向图,入度是以 v 为终点的有向边的数目,记作ID(v)。出度是以 v 为起点的有向边的数目,记作OD(v)。
6.2.1 图的存储-邻接矩阵
数组实现的顺序存储,空间复杂度高,不适合存储稀疏图。
空间复杂度:O(|V|^2) ----只与顶点数有关,和边数无关
适合存储稠密图
#define MaxVertexNum 100 //顶点数目最大值 typedef struct{ char Vex[MaxVertexNum]; //顶点表 int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表 可以用bool型 int vexnum,arcnum; //图的当前顶点数和边数 }MGraph;
带权图:
#define MaxVertexNum 100 #define INFINITY 最大的int值 typedef char VertexType; //顶点的数据类型 typedef int EdgeType; //带权图中边上权值的数据类型 typedef struct{ VertexType Vex[MaxVertexNum]; EdgeType Edge[MaxVertexNum][MaxVertexNum]; //边的权 int vexnum,arcnum; }MGraph;
设图的邻接矩阵为A,则An的元素A [i] [j] 等于由顶点 i 到顶点 j 的长度为 n 的路径的数目。
6.2.2 图的存储-邻接表
//用邻接表存储的图 typedef struct{ AdjList vertices; int vexnum,arcnum; }ALGraph; //顶点 typedef struct VNode{ VertexType data; //顶点信息 ArcNode *first; //第一条边 }VNode,AdjList[MaxVertexNum]; //边 typedef struct ArcNode{ int adjvex; //边指向的顶点 struct ArcNode *next; //指向下一条边的指针 }ArcNode;
6.3.1 图的广度优先遍历(BFS)
要点:
1,找到与一个相邻的所有顶点。
2,标记哪些顶点被访问过。
3,需要一个辅助队列。
时间复杂度:
邻接矩阵:O(|V|2)
邻接表:O(|V|+|E|)
#define MaxVertexNum 100 bool cicited[MaxVertexNum]; //访问标记数组 void BFSTraverse(Graph G){ for(i=0; i<G.vexnum; ++i) visited[i] = false; //访问标记数组初始化 InitQueue(Q); for(i=0; i<G.vexnum; ++i) if(!visited[i]) //对每个联通分量调用一次BFS BFS(G,i); // i 从未访问过,从 i 开始BFS } //广度优先遍历 void BFS(Graph G, int v){ //从 v 出发 visit(v); visited[v] = true; Enqueue(Q,v); while(!isEmpty(Q)){ DeQueue(Q,v); for(w = FirstNeighbor(G,v); w >= 0; w = NextNeighbor(G,v,m)) //检测 v 的所有邻接点 if(!visited[w]){ //w 为 v 的尚未访问的邻接点 visit(w); visited[w] = true; Enqueue(Q,w); } } }
6.3.2 图的深度优先遍历(DFS)
#define MaxVertexNum 100 bool cicited[MaxVertexNum]; //访问标记数组 void DFSTraverse(Graph G){ for(v=0; v<G.vexnum; ++v) visited[v] = false; //访问标记数组初始化 for(v=0; v<G.vexnum; ++v) if(!visited[i]) //对每个联通分量调用一次BFS DFS(G,i); // i 从未访问过,从 i 开始BFS } //深度优先遍历 void DFS(Graph G, int v){ visit(v); visited[v] = true; for(w = FirstNeighbor(G,v); w >= 0; w = NextNeighbor(G,v,m)) if(!visited[w]){ //w 为 v 的尚未访问的邻接点 DFS(G,w); } }
时间复杂度:
邻接矩阵:O(|V|2)
邻接表:O(|V|+|E|)
空间复杂度:O(1)~O(V)
6.4.1 最小生成树
Prim算法:从某一顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直到所有的顶点都纳入为止。
时间复杂度:O(|V|2),适用于边稠密图。
Kruskal算法:每次选择一条权值最小的边,使这条边两头联通,如果原本已经联通,就不选。
时间复杂度:O(|E| log|E|),适用于边稀疏图。
预处理:将边按照权值从小到大排序。
6.4.2 最短路径问题_BFS算法(无权图)
//数组path记录某个顶点的直接前驱 //d数组记录最短路径 void BFS_MIN_Distance(Graph G, int u){ //从 u 出发 //d[i]表示从 u 到 i 的最短路径 for(i=0; i<G.vexnum; i++){ d[i]=无穷; //初始化路径长度 path[i]=-1; //最短路径从哪个顶点进来 } d[u] = 0; visited[u] = true; Enqueue(Q,u); while(!isEmpty(Q)){ DeQueue(Q,u); for(w = FirstNeighbor(G,v); w >= 0; w = NextNeighbor(G,u,w)) //检测 v 的所有邻接点 if(!visited[w]){ //w 为 v 的尚未访问的邻接点 d[w] = d[u]+1; //路径长度+1 path[w] = u; //最短路径应从 u 到 w visited[w] = true; Enqueue(Q,w); } } }
6.4.3 最短路径问题_Dijkstra算法(无权图,带权图)
循环遍历所有结点,找到还没确定最短路径,且dist最小的订单Vi,领final[i] = true。检查所有邻接Vi的顶点,若其final值为false,则更新dist和path的信息。
时间复杂度:O(|V|2)
//数组final标记各顶点是否已经找到最短路径 //数组dist表示目前能找到的最短的路径的总长度 //数组path记录某个顶点的直接前驱
6.4.4 最短路径问题_Floyd算法(无权图,带权图)
对于 n 个顶点的图G,求任意一对顶点Vi->Vj之间的最短路径可分为以下步骤:
初始,不允许中转,最短路径是?
1,若允许在V0中转,最短路径是?
2,若允许在V0, V1中转,最短路径是?
3,若允许在V0, V1, V2中转,最短路径是?
......n轮循环
时间复杂度:O(|V|3)
6.4.5 有向无环图(DAG)描述表达式
顶点中不存在重复的操作数
6.4.6 拓扑排序
AOV(Activity On Vertex NetWork,用顶点表示活动的网):用DAG表示一个工程。顶点表示活动,有向边表示一个活动必须先于另一个活动进行。
实现:
1,从AOV网中选择一个没有前驱的顶点并输出。
2,从网中删除该顶点和以它为起点的边。
3,重复1和2直到AOV为空或当前网中不存在无前驱的顶点为止。
↓
说明有回路
时间复杂度:
邻接矩阵:O(|V|2)
邻接表:O(|V|+|E|)
6.4.7 关键路径
AOV(Activity On Edge NetWork):在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销,称为用边表示活动的网。
性质:
1,只有在某顶点所代表的事件发生后,从该点出发的各有向边所代表的活动才能开始。
2,只有在进入某顶点的各有向边所代表的活动都结束后,该顶点所代表的时间才能发生。
7 查找
7.1.1 查找的基本概念
查找:在数据集合中寻找满足某种条件的数据元素的过程。
查找表:用于查找的数据集合,它由同一类型的数据元素组成。
关键字:数据元素中唯一标识该元素的某个数据项的值,适用基于关键字的查找,结果应该是唯一的。
查找算法的评价指标:
查找长度:在查找运算中,需要对比关键字的次数。
平均查找长度(ASL,Average Search Length):所有查找过程中进行关键字的比较次数的平均值。
7.2.1 顺序查找
顺序查找,又叫线性查找,通常用于线性表。从头到脚一个一个查找。
typedef struct{ ElementType *elem; int TableLen; }SSTable; //顺序查找 int Search_Seq(SSTable ST, ElementType key){ ST.elem[0] = key; //数组下标为0的位置赋值key,称为“哨兵” int i; for(i=ST.TableLen; ST.elem[i] != key; --i); //从后往前 return i; //查找成功,返回下标,查找失败,返回0 }
ASL(成功)=(n+1)/2
ASL(失败)=(n+1)
时间复杂度:O(n)
7.2.2 折半查找
折半查找,又称二分查找,仅适用于有序的顺序表。
typedef struct{ ElementType *elem; int TableLen; }SSTable; //顺序查找 int Binary_Search(SSTable L, ElementType key){ int low = 0, high = L.TableLen-1;, mid; while(low <= high){ mid = (low + high)/2; if(L.elem[mid] == key) return mid; else if(L.elem[mid] > key) high = mid -1; else low = mid +1; } return -1; }
ASL=log2(n+1)-1
时间复杂度:O(log2(n))
7.2.3 分块查找
基本思想:
将查找表分为若干子块。块内元素可以无序,但块间元素使有序的。再建立一个索引表,索引表中每个元素含有各块的最大关键字和各块中第一个元素的地址,索引表按照关键字有序排列。
步骤:
1,在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表。
2,在块内顺序查找。
顺序+顺序:
ASL=(b+1)/2 + (s+1)/2 = (s2+2s+n)/2s, 当s=n1/2时,ASL最小 = n1/2+1
折半+顺序:
ASL=log2(b+1)(向上取整)+(s+1)/2
7.3.1 二叉排序树
二叉排序树,又称二叉查找树(BST)
性质:
1,左子树上所有结点的关键字均小于根结点的关键字。
2,右子树上所有结点的关键字均大于根结点的关键字。
3,左右子树各自是一棵二叉排序树。
typedef struct BSTNode{ int key; struct BSTNode *lchild,*rchild; }BSTNode,*BSTree; //在二叉排序树中查找值为key的结点 BSTNode *BST_Search(BSTree T, int key){ while(T != null && k!= T->key){ if(key < T->key) T = T->lchild; else T = T->rchild; } return T; }
二叉排序树的插入:
// int BST_Insert(BSTree &T, int k){ if(T == null){ //原树为空,新插入的结点为根结点 T=(BSTree)malloc(sizeof(BSTNode)); T->key = k; T->lchild = T->rchild = null; return 1; } else if(k == T->key) //原树中存在相同的值,插入失败 return 0; else if(k< T->key) return BST_Insert(T->lchild,k); else return BST_Insert(T->rchild,k); }
二叉排序树的删除:
1,若被删除结点是叶结点,直接删除
2,若被删除结点只有左子树或只有右子树,则让它的子树代替它的位置
3,若被删除结点既有左子树又有右子树,则令它的直接后继(或直接前驱)代替它,然后从二叉树中删去这个直接后继(或直接前驱)
7.3.2 平衡二叉树(AVL)
平衡二叉树:树上任一结点的左子树和右子树的高度之差不超过1.
结点的平衡因子=左子树高-右子树高
typedef struct AVLNode{ int key; int balance; //平衡因子 struct AVLNode *lchild, *rchild; }AVLNode;*AVLTree;
7.3.3 平衡二叉树的删除
步骤:
1,删除结点
1,若叶结点,直接删。
2,若删除的结点只有一个子树,用子树顶替删除位置
3,若删除的结点有两个子树,用前驱(或后继)结点代替,并转换为对前驱(或后继)结点的删除
2,一路向北找到最小不平衡子树
3,找到最小不平衡子树下最高的儿子,孙子
4,根据孙子的位置,调整平衡
5,如果不平衡向上传导,继续2
7.3.4 红黑树的定义和性质
定义:
1,每个结点是红色的或黑色的。
2,根结点是黑色的。
3,叶结点(外部结点、null结点、失败结点)都是黑色的。
4,不存在两个相邻的红结点(红结点的父节点和子结点都是黑色的)。
5,对每个结点,从该结点到任一叶结点的简单路径上,所含黑结点的数目相同。
性质:
1,从根结点到叶结点的最长路径不大于最短路径的2倍。
2,有 n 个内部结点的红黑树高度 h < 2log2(n+1)
时间复杂度:O(log2n)
struct RBnode{ int key; RBnode* parent; RBnode* lchild; RBnode* rchild; int color; }
7.3.5 红黑树的插入
步骤:
1,查找,确定插入位置。
2,新结点是根->染为黑色。
3,新结点非根->染为红色
插入新结点后依然满足红黑树性质,则插入结束。
插入后不满足性质,调整
如何调整:看叔叔
黑叔叔:旋转+染色
LL型:右单旋,父换爷+染色
RR型:左单旋,父换爷+染色
LR型:左、右双旋,儿换爷+染色
RL型:右、左双旋,儿换爷+染色
红叔叔:染色+变新
叔父爷染色,爷变为新结点
7.3.6 红黑树的删除
时间复杂度:O(log2n)
处理方式和二叉排序树的删除一样。
7.4.1 B树
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用 m 表示,一棵 m 阶B树要么空,要么有以下性质:
1,树中每个结点至多有 m 棵子树,也就是至多含有 m-1 个关键字。
2,若根结点不是终端结点,则至少有两棵子树。
3,除根结点外的所有非叶结点至少有(m/2)(向上取整)棵子树,也就是至少有(m/2)(向上取整)-1 个关键字。
4,所有非叶结点的结构如下:
n | P0 | K1 | P1 | K2 | P2 | …… | Kn | Pn |
其中 n 为关键字的个数,Ki为关键字,Pi为指针
5,所有的叶结点都在同一层,且不带信息。
含有 n 个关键字的 m 阶B树:
最小高度:h >= logm(n+1)
最大高度:h <= log|m/2|[(n+1)/2] +1
7.4.2 B树的插入和删除
插入:
1,定位
2,插入,每个非根结点的关键字个数为(m/2)↑+1到m-1,若插入后关键字个数小于m,可以直接插入,否则分裂,把(m/2)↑位置的结点插入父结点。
删除:
1,若被删除关键字在终端结点,直接删除该关键字,但注意关键字个数范围。
2,若被删除关键字在非终端结点,用直接前驱或直接后继来替代被删除的关键字
7.4.3 B+树
一棵 m 阶的B+树要满足以下条件:
1,每个分支节点最多有m棵子树。
2,非叶根结点至少有两棵子树,其他分支结点至少有(m/2)↑棵子树。
3,结点的子树个数和关键字个数相等。
4,所有的叶结点包含全部的关键字以及指向响应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互连接。
5,所有分支结点中仅包含各个子节点中关键字的最大值以及指向其子结点的指针。
7.5.1 散列查找
散列表:又叫哈希表,数据元素的关键字与其存储地址直接相关。
不同的关键字通过散列函数映射到同一个值,则称他们为同义词。
通过散列函数确定的位置已经存放了其他元素,这种情况叫冲突。
处理冲突的方法:
拉链法:把所有的同义词存储在一个链表里。
开放定址法:空闲地址向所有的同义词开放
线性探测法:冲突就向后一位寻找空闲
平方探测法:偏移量为:12,-12,22,-22,32,-32...
8 排序
8.1.1 排序的基本概念
排序:重新排列表中的元素,使表中的元素满足按关键字有序的过程。
8.2.1 插入排序
直接插入排序:
void InsertSort(int A[], int n){ int i, j, temp; for(i = 1; i <n; i++) if(A[i]<A[i-1]){ temp = A[i]; //用temp暂存A[i] for(j = i-1; j>=0 && A[j]>temp ; --j) //检查前面所有已排好序的元素 A[j+1] = A[j]; //所有大于temp的元素向后挪位 A[j+1]=temp; } }
直接插入排序(带哨兵):
void InsertSort(int A[], int n){ int i, j; for(i = 2; i <n; i++) if(A[i]<A[i-1]){ A[0] = A[i]; //复制为哨兵 for(j = i-1; A[0]<A[j] ; --j) A[j+1] = A[j]; A[j+1]=A[0]; } }
空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:稳定
优化:折半插入排序:
思路:先用折半查找找到应该插入的位置,再移动元素。
当low>high时折半查找停止,将[low,i-1]内的元素全部右移,并将A[0]复制到low所指位置。
当A[mid] == A[0] 时,为了保证算法的稳定性,应继续在mid所指位置的右边寻找插入位置。
void InsertSort(int A[], int n){ int i, j, low, high, mid; for(i = 2; i <=n; i++){ A[0] = A[i]; low =1; high = i-1; while(low <= higj){ //折半查找 mid = (low+high)/2; if(A[mid] > A[0]) high = mid -1; else //包含了保证算法稳定性的逻辑 low = mid + 1; } for(j = i-1; j >= high+1 ; --j) A[j+1] = A[j]; A[high+1]=A[0]; //插入 } }
空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:稳定
8.2.2 希尔排序
先追求表中元素部分有序,再逐渐逼近全局有序。
void ShellSort(int A[], int n){ int d, i, j; //A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到 for(d = n/2; d >= 1; d = d/2) //步长变化 for(i=d+1; i<=n; ++i) // i 的初始值指向第一个子表的第二个元素 if(A[i] < A[i-d]){ //将A[i]插入有序增量子表 A[0] = A[i]; for(j=i-d; j>0 && A[0]<A[j]; j-=d) A[j+d] = A[j]; //后移到j+d的位置 A[j+d] = A[0]; } }
空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:不稳定
适用性:仅适用于顺序表,不使用于链表
8.3.1 冒泡排序
//交换 void swap(int &a, int &b){ int temp = a; a = b; b = temp; } void BubbleSort(int A[], int n){ for(int i=1; i < n-1; i++){ bool flag = false; //本躺冒泡是否发生交换的标志 for(int j=n-1; j > i; j--) if(A[j-1] > A[j]){ //逆序 swap(A[j-1], A[j]); flag = true; } if(flag == false) //本躺没有交换,说明已经有序 return; } }
空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:稳定
适用性:适用于顺序表和链表
8.3.2 快速排序
//用第一个元素将待排序序列划分成左右两个部分 int Partition(int A[], int low, int high){ int pivot = A[low]; //第一个元素作为枢纽 while(low < high){ //用 low 和 high 搜索枢纽的最终位置 while(low < high && A[high] >= pivot) --high; A[low] = A[high]; //比枢轴小的元素移动到左端 while(low < high && A[high] <= pivot) ++low; A[high] = A[low]; //比枢轴大的元素移动到右端 } A[low] = pivot; //枢轴元素存放到最终位置 return low; //返回枢轴的位置 } void QuickSort(int A[], int low, int high){ if(low < high){ //递归跳出的条件 int pivotpos = Partition(A, low, high); //划分 QuickSort(A, low, pivotpos-1); //划分左子表 QuickSort(A, pivotpos+1, high); //划分右子表 } }
空间复杂度:O(递归层数) 最好:O(log2n) 最坏:O(n)
时间复杂度:O(n*递归层数) 最好:O(nlog2n) 最坏:O(n2)
稳定性:不稳定
适用性:适用于顺序表和链表
优化思路:尽量选择可以把数据中分的枢轴元素
1,选头,中,尾三个元素的平均值
2,随机选一个元素
8.4.1 简单选择排序
每一趟在待排序元素中选取关键字最大的元素加入有序子序列
void SelectSort(int A[], int n){ for(int i = 0; i < n-1; i++){ //一共进行 n-1 趟 int min = i; //记录最小元素的位置 for(int j = i+1; j < n; j++) if(A[j] < A[min]) //找到无序序列中最小的元素 min = j; //更新最小元素的位置 if(min != i) swap(A[j], A[min]); } }
空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:不稳定
适用性:适用于顺序表和链表,以及关键字较少的情况
8.4.2 堆排序
每一趟将堆顶元素加入有序子序列(与待排序序列中最后一个元素交换),并将待排序序列再次调整为大顶堆。
n-1趟处理后,得到递增序列。
//建立大顶堆 void BuildMaxHeap(int A[], int len){ for(int i = len/2; i>0; i--) //从后往前调整左右非终端结点 HeadAdjust(A, i, len); } //将以 k 为根的子树调整为大顶堆 void HeadAdjust(int A[], int k, int len){ A[0] = A[k]; //A[0]暂存子树的根结点 for(int i=2*k; i <= len; i *= 2){ // i 指向当前结点的左孩子 if(i < len && A[i] < A[i+1]) //比较左右孩子谁更大 i++; if(A[0] >= A[i]) //根结点比最大的孩子大,当前结点满足条件,跳出循环 break; else{ A[k] = A[i]; //将A[i]换带双亲结点 k = i; //修改 k 值,继续查看修改后的子结点是否满足大顶堆的条件 } A[k] = A[0]; //将被筛选结点的值放入最终位置 } } //堆排序 void HeapSort(int A[], int len){ BuildMaxHeap(A, len); for(int i=len; i>1; i--){ swap(A[i], A[1]); HeadAdjust(A, 1, i-1); //重新整理成堆 } }
空间复杂度:O(1)
时间复杂度:O(nlog2n)
稳定性:不稳定
适用性:仅适用于顺序表
堆的删除:被删除的元素用堆底元素代替,然后调整堆
8.5.1 归并排序
int *B = (int *)malloc(n*sizeof(int)); //辅助数组B void Merge(int A[], int low, int mid, int high){ int i, j, k; for(k = low; k <= high; k++) B[k] = A[k]; //将 A 中所有的元素复制到 B 中 for(i = low, j = mid+1, k=i; i <= mid && j <=high; k++){ // i 指向 B 中第一个序列的首元素, j 指向 B 中第二个序列的首元素 // k 指向 A 中下一个空位 //把小的赋值给A[k],并且指针后移 if(B[i] <= B[j]) A[k] = B[i++]; else A[k] = B[j++]; } //当只剩下一个序列时,直接全部赋值 while(i <= mid) A[k++] = B[i++]; while(j <= high) A[k++] = B[j++]; } void MergeSort(int A[], int low, int high){ if(low < high){ int mid = (low+high)/2; //从中间划分 MergeSort(A, low, mid); //左半部分归并排序 MergeSort(A, mid+1, high); //右半部分归并排序 Merge(A, low, mid, high); //归并 } }
空间复杂度:O(n)
时间复杂度:O(nlog2n)
稳定性:稳定
适用性:适用于顺序表和链表
8.5.2 基数排序
步骤:
1,初始化:设 r 个空队列。按照各个关键字位权重递增的次序,对所有关键字位分别“分配”和“收集”
2,分配:顺序扫描各个元素,按当前处理的关键字位插入对应的队尾
3,收集:各队依次出队
空间复杂度:O(n)
时间复杂度:O(d(n+r))
稳定性:稳定
适用性:适用于顺序表和链表