数据结构
一、绪论
1.基本概念和术语
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合(结构就是关系)
数据结构:分为逻辑结构(集合结构、树形结构、线性结构、图形结构)和物理结构(数据的逻辑结构在计算机中的存储形式,也叫存储结构)
存储结构:顺序存储和链式存储。
2.抽象数据类型
数据类型:是一组性质相同的值的集合以及定义于这个值
数据类型的作用:
-
1.约束变量的内存空间
-
2.约束变量或常量的取值范围
-
3.约束变量或常量的操作
抽象数据类型:对已有数据结构进行抽象(基本数据类型进行打包封装)
抽象数据类型的概念与面向对象的思想是一致的。
3.算法
程序=数据结构+算法
算法:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作
时间复杂度:T(n)=O(f(n))
省略常系数,只保留最高项
常见时间复杂度比较:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n!)<O(nn)
空间复杂度:S(n)=O(f(n))
算法的空间复杂度通过计算算法所需的存储空间实现,所需的存储空间指的是一个算法在运行过程中的临时占用存储空间
4.组织架构
数据结构可以进一步分为线性结构和图形结构(树是一种特殊的图)
二、线性表(一般线性表)
1.基本概念
(1)定义:线性表就是n个数据元素(类型相同)的有限序列
(2)特点:存在唯一的一个被称作“第一个”的数据元素 存在唯一的一个被称作“最后一个”的数据元素
除第一个之外的数据元素均只有一个前驱 除最后一个之外的数据元素均只有一个后继
存在首尾,除首尾都有前驱和后继
2.顺序存储结构
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素 ,这种方法存储的线性表称作顺序表。
描述顺序存储结构需要三个属性:
(1)存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
(2)线性表的最大存储容量:数组长度MaxSize。
(3)线性表的当前长度:length。
顺序表存入或者取出数据时间复杂度为O(1)(通过公式直接计算),这种存储结构称为随机存取结构
优缺点
优点:(1)无需为表中元素之间的逻辑关系增加额外的空间 (2)快速查找
缺点:(1)插入删除移动大量元素 (2)存储空间固定
//顺序存储结构线性表
include<stdio.h>
include<stdlib.h>
define MAXSIZE 20
typedef struct{
int data[MAXSIZE];
int length;
}Sqlist;
/*
typedef struct{
int*data;
int MAXSIZE;
int length;
}Sqlist;
*/
//线性表的初始化
void Initlist(Sqlist* p) {
p->length = 0;
}
/*
void Initlist(Sqlist* p){
p->data = (int*)malloc(sizeof(int));
if(p->data==NULL)
{
printf("动态内存分配失败");
exit(-1);
}
p->length = 0;
}
*/
//线性表的创建
void CreateList(Sqlist* p,int n) {
int i,m;
for (i = 0; i < n; i++)
{
scanf("%d", &m);
p->data[i] = m;
p->length++;
}
}
//获取元素操作
int GetElem(Sqlist* p, int e) {
int i; //第几个元素
scanf("%d", &i);
if (i > p->length)
{
exit(0);
}
e = p->data[i - 1];
return e;
}
顺序存储结构线性表中
插入一个元素,平均需要移动 n/2个元素
删除一个元素,平均需要移动 n-1/2个元素
//插入元素
算法思路:
如果插入位置不合理,抛出异常;
如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
将要插入元素填入位置i处,表长加1。
Sqlist* ListInsert(Sqlist* p) {
int k,i,a;
scanf("%d", &i);
scanf("%d", &a);
if (p->length == MAXSIZE)
{
printf("线性表已满");
exit(0);
}
if (i<1 || i>p->length + 1)
{
printf("插入位置不在范围内 ");
exit("error");
}
if (i <= p->length)
{
//将要插入位置后数据元素向后移动一位
for (k = p->length - 1; k >= i - 1; k--)
{
p->data[k + 1] = p->data[k];
}
p->data[i - 1] = a;
p->length++;
return p;
}
}
//删除元素
如果删除位置不合理,抛出异常;
取出删除元素;
从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
表长减1。
Sqlist* ListDelete(Sqlist* p) {
int k, i;
scanf("%d", &i);
if (p->length == 0) //线性表为空
{
exit(0);
}
if (i<1 || i>p->length + 1)
{
printf("删除位置不在范围内 ");
exit(0);
}
if (i < p->length)
{
for (k = i; k < p->length; k++)
{
p->data[k - 1] = p->data[k];
}
}
p->length--;
return p;
}
//插入和删除的平均时间复杂度均为O(n)
int main(void)
{
int n,j,e = 0;//初始化e
Sqlist* p = (Sqlist*)malloc(sizeof(Sqlist)); //初始化p
Initlist(p);
scanf("%d", &n);
CreateList(p, n);
//e = GetElem(p, e);
//p = ListInsert(p);
//p = ListDelete(p);
for (j = 0; j < p->length; j++)
{
printf("%d ", p->data[j]);
}
return 0;
}
3.链式存储结构
用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。
头结点与头指针
头指针是指向链表第一个结点的指针,如果有头结点就是指向头结点的指针,具有标识作用,常被冠以链表名字
头指针是必须有的,头结点不是必须有的
头结点是为了统一第一个元素与其他元素插入删除操作的统一
由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是**“工作指针后移”**
//链式存储结构线性表
#include<stdio.h>
#include<stdlib.h>
typedef struct Lnode {
int data;
struct Lnode* next;
}Lnode, * LinkList;
//链式结构线性表不用规定长度(没有length),存储的元素个数不受限制(没有MAXSIZE)
//初始化
LinkList InitList(LinkList h) {
h = (LinkList)malloc(sizeof(Lnode));
if (!h)
{
exit(-1);
}
h->next = NULL;
}
//尾插法创建单链表
LinkList CreateList(LinkList h) {
LinkList p,s;
int i,len;
scanf("%d", &len);
p = h;
for (i = 0; i < len; i++)
{
s = (LinkList)malloc(sizeof(Lnode));
scanf("%d", &s->data);
s->next = NULL;
p->next = s;
p = s;
}
return h;
}
//头插法创建
//LinkList CreateList(LinkList h) {
// LinkList s;
// int i, len;
// scanf("%d", &len);
// for (i = 0; i < len; i++)
// {
// s = (LinkList)malloc(sizeof(Lnode));
// scanf("%d", &s->data);
// s->next = h->next;
// h->next = s;
// }
// return h;
//}
//读取、遍历和查找
查找算法思路
1.声明一个指针p指向链表第一个结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3.若到链表末尾p为空,则说明第i个结点不存在;
4.否则查找成功,返回结点p的数据。
int GetElem(LinkList h) {
int i, j=1, e;
LinkList p; //声明一个指针p指向链表第一个结点,初始化j从1开始
scanf("%d", &i);
p = h->next;
while (p && j < i)
{
p = p->next;
j++;
}
if (!h || j > i)
{
exit(0); //第i个节点不存在
}
e = p->data;
return e;
}
//插入元素
LinkList ListInsert(LinkList h) {
int i, j=1, e;
LinkList s;
LinkList p = h;
scanf("%d", &i);
scanf("%d", &e);
while (p && j < i) //寻找第i-1个结点
{
p = p->next;
j++;
}
if (!p || j > i)
{
exit("error");
}
s = (LinkList)malloc(sizeof(Lnode));
s->data = e;
s->next = p->next;
p->next = s;
return h;
}
//删除元素
LinkList ListDelete(LinkList h) {
int i, j = 1;
LinkList p = h;
LinkList q;
scanf("%d", &i);
while (p->next && j < i)//寻找第i-1个结点
{
p = p->next;
j++;
}
if (!(p->next) || j > i)
{
exit("error");
}
q = p->next;
p->next = q->next;
//e = q->data
free(q);
return h;
}
//单链表整表删除
LinkList ClearList(LinkList h) {
LinkList p, q;
p = h->next;
while (p)
{
q = p->next;
free(p);
p = q;
}
h->next = NULL;
return h;
}
int main(void)
{
int i;
LinkList h = (LinkList)malloc(sizeof(Lnode));
h = InitList(h);
h = CreateList(h);
h = h->next;
while (h != NULL)
{
printf("%d ", h->data);
h = h->next;
}
return 0;
}
//循环删除线性表重复元素
Void A(LinkList* head) //head是无表头附加结点的单向链表
{
LinkList* p, * q, * pre, * r;
p = head->next;
While(p != NULL)
{
q = p->next; pre = p;
While(q != NULL)
{
if (p->data = = q->data)
{
r = q; pre->next = q->next;
q = q->next; free(r);
}
else
{
pre = q; q = q->next;
}
}
p = p->next;
}
}
4.静态链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVnbLIWu-1646652039912)(数据结构.assets/1638442807669.png)]
//静态链表
//由于没有指针,用数组来代替指针,来描述单链表。
#include<stdio.h>
#include<stdlib.h>
typedef struct
{
int data;
int cur;
}component, SLinkList[100];
//模拟申请内存
int Malloc(SLinkList space)
{
int i = space[0].cur;
if (i)
space[0].cur = space[i].cur; //得把它的下一个分量用来做备用
return i; //返回的第一个备用空闲的下标
}
//模拟释放内存
void Free(SLinkList space, int k)
{
space[k].cur = space[0].cur; // 把第一个元素cur值赋给要删除的分量cur,相当于回顾到备用内存
space[0].cur = k; //让这个删除的位置成为第一个优先空位,把它存入第一个元素的cur中
}
//创建并初始化静态链表
void Creat(SLinkList L)
{
int i;
L[99].cur = 0; //备用链表的头指针是空间的第一个位置。0 在静态链表中也意味着NULL;
for (i = 0; i < 98; i++)
L[i].cur = i + 1;
L[98].cur = 0;//最后一个指针的cur也是0。
}
//求静态链表长度
int ListLength(SLinkList L)
{
int i = 0, k = L[99].cur; //头指针
while (k) //头指针不为零时
{
k = L[k].cur;
i++;
}
return i;
}
//插入
void Insert(SLinkList L, int val, int index){
int i = 99, k, n;
k = Malloc(L); //申请内存
if (k){
L[k].data = val;
for (n = 1; n < index; n++)
i = L[i].cur; //头指针遍历找到前一个
L[k].cur = L[i].cur; //连接后面
L[i].cur = k; //连接前面
}
}
//遍历打印
void Traverse(SLinkList L) {
int i = L[99].cur;
while (i)
{
printf("%d", L[i].data);
i = L[i].cur;
}
}
//删除
void ListDelete(SLinkList L, int val) {
int j, k;
/*if (val<1 || val>Listlength) //判断是否在范围内
{
return;
}*/
k = 99; //MAXSIZE-1
for (j = 1; j <= val; j++) {
k = L[k].cur;
L[k].cur = L[j].cur;
Free(L, val);
}
}
5.循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表和单链表的主要差异在循环的判断条件上,原来是判断p->next是否为空,现在则是p- >next不等于头结点,则循环未结束。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fHekR3DX-1646652039913)(数据结构.assets/1638442459236.png)]
在单链表中有了头结点,可以用O(1)的时间访问第一个结点,但访问最后一个结点要O(n)
改造循环链表,不用头指针,用指向终端结点的尾指针来表示循环链表
查找终端结点是O(1) 查找开始结点rear- >next->next 也是O(1)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uiNmJLlb-1646652039914)(数据结构.assets/1638442722262.png)]
//循环链表
#include <stdio.h>
#include <stdlib.h>
//循环链表实现
typedef struct node {
int data;
struct node* next;
int size; //成环后定义的链表长度确定,需要定义size来标明
}node;
//循环链表初始化
node* creastList(node* L, int size)
{
if (NULL == L)
{
L = (node*)malloc(sizeof(node));
if (!L)
{
exit(-1);
}
L->next = L; //尾指向头,体现循环
return L;
}
else
{
exit(-1);
}
}
//插入(分头插与尾插,NULL变成头)和删除与单链表相似
//两个循环链表的合并
node* addList(node* A, node* B) {//传入的是尾指针
node* p, * q;
p = A->next; //保留A的头结点
A->next = B->next->next; //B的首元结点连在A的尾部
//前面是框,后面是值
q = B->next; //将B的头结点赋值给q,便于释放
B->next = p; //A连B尾
free(q);
return A;
}
6.双向循环链表
为克服单链表的单向性,设计出双向链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T5mLlgFB-1646652039915)(数据结构.assets/1638442968570.png)]
//双向循环链表
#include <stdio.h>
#include <stdlib.h>
typedef struct dulnode {
int data;
struct dulnode* prior;
struct dulnode* next;
}dulnode;
//插入
dulnode* insertnode(dulnode* L, int i) {
int k;
dulnode* s = (dulnode*)malloc(sizeof(dulnode));
for (k = 1; k < i; k++)//找到前一个
{
L = L->next;
}
s->prior = L; //填s的前指针域
s->next = L->next; 填s的后指针域
L->next->prior = s; //填插后的前指针域
L->next = s; //填插前的后指针域
}
先搞定s的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。
//删除
/*
L->prior->next = L->next;
L->next->prior = L->prior;
free(L);
*/
三、栈(操作受限线性表)
1.基本概念
栈是限定仅在表尾进行插入和删除操作的线性表,是一种特殊的线性表
允许插入和删除的一端称为栈顶,另一端叫栈底(线性表表尾叫栈顶,表头叫栈底)
栈又被称为后进先出的线性表,简称LIFO结构
栈的插入操作:入栈 栈的删除操作:出栈
出栈次序多种多样
2.顺序存储结构
#include<stdio.h>
#include<stdlib.h>
#define MAXZIZE 50
typedef struct sqstack {
int data[MAXZIZE];
int top; //栈顶指针
}sqstack;
//初始化
void initstack(sqstack* L) {
L->top = -1; //初始化数值不同(0,1),之后的操作也不同
}
//入栈
void push(sqstack* L, int i) {
if (L->top == MAXZIZE - 1) { //判满
return;
}
L->top++; //先++,再赋值
L->data[L->top] = i;
//L->data[++L->top] = i; 等价
}
//出栈
void pop(sqstack* L, int e) {
if (L->top == -1) {//判空
return;
}
e = L->data[L->top]; //先赋值再--
L->top--;
}
3.链式存储结构
//链式栈
#include<stdio.h>
#include<stdlib.h>
typedef struct stacknode {
int data;
struct stacknode* next;
}stacknode, * linkstackptr;
typedef struct linkstack {
linkstackptr top;
int count;
}linkstack;
//初始化
void initstack(linkstack *s) {
s->count = 0;
}
//入栈(入栈没有判满,出栈有判空)
linkstack* push(linkstack* s, int e) {
linkstackptr p = (linkstackptr)malloc(sizeof(stacknode));
//如果栈空
if (s->count == 0) {
s->top = p;
p->data = e;
p->next = NULL;
}
else {
p->data = e;
p->next = s->top;
s->top = p;
}
s->count++;
return s;
}
//出栈
linkstack* pop(linkstack* s, int e) {
linkstackptr p; //释放tsg
if (s->count == 0) {
exit(-1);
}
e = s->top->data;
p = s->top;
s->top = s->top->next;
free(p);
s->count--;
return s;
}
4.栈的应用
生活中的应用:手枪,网站的后退键,撤销操作
用栈求4则运算
定义一个数值栈,一个符号栈,表达式的数值入数值栈,运算符号入符号栈,当符号的优先级大于栈内符号,入栈,小于或者等于,从数值栈内取两个数进行计算,得到的值放入数值栈,按规则从左到右计算
//栈的应用
//1.递归:斐波那契数列
//数组实现
#include<stdio.h>
int main(void)
{
int i;
int a[40];
printf("迭代显示斐波那契数列:\n");
a[0] = 0;
a[1] = 1;
printf("%d ", a[0]);
printf("%d ", a[1]);
for (i = 2; i < 40; i++)
{
a[i] = a[i - 1] + a[i - 2];
printf("%d ", a[i]);
}
return 0;
}
//递归实现
int Fbi(int i) /* 斐波那契的递归函数 */
{
if (i < 2)
return i == 0 ? 0 : 1;
return Fbi(i - 1) + Fbi(i - 2); /* 这里Fbi就是函数自己,等于在调用自己 */
}
int main(void)
{
int i;
for (i = 0; i < 40; i++)
printf("%d ", Fbi(i));
return 0;
}
//汉诺塔
#include<stdio.h>
void Hanio_Step(int n, char A, char B, char C)
{
if (1 == n)
printf("%c->%c\n", A, C);
else
{
Hanio_Step(n - 1, A, C, B);
printf("%c->%c", A, C);
Hanio_Step(n - 1, B, A, C);
}
}
int main(void)
{
int n = 0;
scanf("%d", &n);
Hanio_Step(n, 'A', 'B', 'C');
return 0;
}
共享栈
用一个数组来存储两个栈,数组的两个端点(下标为0处,数组长度为n-1处)为两个栈的栈底, 两个栈如果增加元素,就是两端点向中间延伸。
栈1为空:top1=-1 栈2为空:top2=n
两个栈见面之时,也就是两个指针之间相差1时,即 top1+1==top2为栈满。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfOiS0su-1646652039916)(数据结构.assets/1638444739368.png)]
/* 两栈共享空间结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top1; /* 栈1栈顶指针 */
int top2; /* 栈2栈顶指针 */
} SqDoubleStack;
//插入
Status Push(SqDoubleStack *S, SElemType e,
int stackNumber)
{
/* 栈已满,不能再push新元素了 */
if (S->top1 + 1 == S->top2)
return ERROR;
/* 栈1有元素进栈 */
if (stackNumber == 1)
/* 若栈1则先top1+1后给数组元素赋值 */
S->data[++S->top1] = e;
/* 栈2有元素进栈 */
else if (stackNumber == 2)
/* 若栈2则先top2-1后给数组元素赋值 */
S->data[--S->top2] = e;
return OK;
}
//删除
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
if (stackNumber == 1)
{
/* 说明栈1已经是空栈,溢出 */
if (S->top1 == -1)
return ERROR;
/* 将栈1的栈顶元素出栈 */
*e = S->data[S->top1--];
}
else if (stackNumber == 2)
{
/* 说明栈2已经是空栈,溢出 */
if (S->top2 == MAXSIZE)
return ERROR;
/* 将栈2的栈顶元素出栈 */
*e = S->data[S->top2++];
}
return OK;
}
四、队列(操作受限线性表)
1.基本概念
队列:只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
是一种先进先出的线性表,简称FIFO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B3BW8Coe-1646652039916)(数据结构.assets/1638445656729.png)]
2.顺序存储结构
为了避免数组插入和删除时需要移动数据,解决假溢出和数组下标越界的问题,设计出循环队列
//循环队列
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 10
typedef struct queue {
int data[MAXSIZE];
int front;
int rear;
}queue;
//初始化
void initqueue(queue* q) {
q->front = 0;
q->rear = 0;
}
//求队列长度
int queuelength(queue* q) {
int i;
i = (q->rear - q->front + MAXSIZE) % MAXSIZE;
return i;
}
//入队
void inputqueue(queue* q,int e) {
if ((q->rear + 1) % MAXSIZE == q->front) {
exit(-1); //判满
}
q->data[q->rear] = e;
q->rear = (q->rear + 1) % MAXSIZE;
//rear指针向后移一位置
}
//出队
int outputqueue(queue* q, int e) {
//判空
if (q->front == q->rear) {
return;
}
e = q->data[q->front];
q->front = (q->front + 1) % MAXSIZE;
return e;
}
3.链式存储结构
#include<stdio.h>
#include<stdlib.h>
typedef struct node {
int data;
struct node* next;
}node, * queuestr;
typedef struct linkqueue {
queuestr front, rear;
}linkqueue;
//初始化
void initlinkqueue(linkqueue* q) {
q->front = (linkqueue*)malloc(sizeof(linkqueue));
if (!q->front) {
exit("error");
}
q->rear = q->front;//q->front相当于链表的头指针
q->front->next = NULL;
}
//入队
void inputlinkqueue(linkqueue* q, int e) {
queuestr s = (queuestr)malloc(sizeof(node));
if (!s) {
exit("overflow");
}
s->data = e; //尾插法
s->next = NULL;
q->rear->next = s;
q->rear = s;
}
//出队
int outputlinkqueue(linkqueue* q, int e) {
queuestr p;
if (q->front == q->rear) { //判空
return;
}
p = q->front->next; //首元结点
e = p->data;
q->front->next = p->next;
if (q->rear == p) {// 若队头是队尾,则删除后将rear指向头结点
q->rear = q->front;
}
free(p);
return e;
}
五、串(数据受限线性表)
1.基本概念
串是由零个或多个字符组成的有限序列,又名叫字符串。
空串:长度为0,直接用两个双引号表示
子串与主串
串的比较:比较组成串的字符编码
2.串的定长顺序存储表示
#include<stdio.h>
#include<stdlib.h>
#define MAXLEN 40
typedef char SString[MAXLEN + 1];
//1.生成一个其值等于chars的串T
SString* StrAssign(SString T, char* chars) {
int i;
if (strlen(chars) > MAXLEN)//chars的长度大于最大串长
exit("error");
else
{
T[0] = strlen(chars);//0号单元存放串的长度
for (i = 1; i < T[0]; i++)//从1号单元起复制串的内容
T[i] = *(chars + i - 1);
return T;
}
}
//2.由串S复制得串T
void StrCopy(SString T, SString S) {
int i;
for (i = 0; i < S[0]; i++)
T[i] = S[i];
}
//3.若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0
int StrCompare(SString S, SString T) {
int i;
for (i = 1; i <= S[0] && i <= T[0]; ++i)
{
if (S[i] != T[i])
return S[i] - T[i];
}
return S[0];
}
//4.用T返回S1和S2连接而成的新串。若未截断,则返回TRUE;否则返回FALSE
SString* Concat(SString T, SString S1, SString S2) {
int i;
if (S1[0] + S2[0] <= MAXLEN) {//未截断
for (i = 1; i <= S1[0]; i++) {
T[i] = S1[i];
}
for (i = 1; i <= S2[0]; i++) {
T[S1[0] + i] = S2[i];
}
T[0] = S1[0] + S2[0];
return T;
}
else//截断S2
{
for (i = 1; i <= S1[0]; i++) {
T[i] = S1[i];
}
for (i = 1; i <= MAXLEN - S1[0]; i++)//到串长为止
T[S1[0] + i] = S2[i];
T[0] = MAXLEN;
return T;
}
}
//5.用Sub返回串S的自第pos个字符起长度为len的子串
SString* SubString(SString Sub, SString S, int pos, int len) {
int i;
if (pos<1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)//pos和len的值超出范围
exit("error");
for (i = 1; i <= len; i++)
Sub[i] = S[pos + i - 1];
Sub[0] = len;
return Sub;
}
3.串的堆分配顺序存储表示
//堆分配存储结构的串既有顺序存储结构的特点,处理方便,操作中对串长又没有任何限制,更显灵活。
typedef struct HString {
char* ch;
int len;
}HString;
4.串的块链存储表示
typedef struct chunk {
char ch[80];
struct chunk* next;
}chunk;
typedef struct {
chunk* head, * tail;//串的头和尾指针
int curlen;//串的当前长度
}LString;
5.模式匹配算法
1.朴素模式匹配算法
返回子串T在主串S中第pos个字符之后的位置,若不存在,则函数值为0
int Index1(SString S, SString T, int pos) {
int i, j;
if (1 <= pos && pos <= S[0]) {//pos的范围合适
i = pos;//从主串S的第pos个字符开始和子串T的第1个字符比较
j = 1;
while (i <= S[0] && j <= T[0])
if (S[i] == T[j]) {//当前两个字符相等
++i;//继续比较后继字符
++j;
}
else//当前两个字符不相等
{
i = i - j + 2;//两指针后退重新开始匹配
j = 1;
}
if (j > T[0])//主串S中存在子串T
return i - T[0];
else//主串S中不存在子串T
return 0;
}
else//pos的范围不合适
return 0;
}
2.KMP模式匹配算法
#include<stdio.h>
#include<stdlib.h>
#define MAXLEN 40
typedef char string[MAXLEN + 1];
//求next数组
void getnext(string T, int* next) {
int i, j;
i = 1;
j = 0;
next[1] = 0;
/* 此处T[0]表示串T的长度 */
while (i < T[0])
{
/* T[i]表示后缀的单个字符, */
/* T[j]表示前缀的单个字符 */
if (j == 0 || T[i] == T[j])
{
++i;
++j;
next[i] = j;
}
else
/* 若字符不相同,则j值回溯 */
j = next[j];
}
}
//KMP模式匹配算法
int Index_KMP(string S, string T, int pos)
{
/* i用于主串S当前位置下标值,若pos不为1, */
/* 则从pos位置开始匹配 */
int i = pos;
/* j用于子串T中当前位置下标值 */
int j = 1;
/* 定义一next数组 */
int next[255];
/* 对串T作分析,得到next数组 */
get_next(T, next);
/* 若i小于S的长度且j小于T的长度时, */
/* 循环继续 */
while (i <= S[0] && j <= T[0])
{
/* 两字母相等则继续,相对于朴素算法增加了 */
/* j=0判断 */
if (j == 0 || S[i] == T[j])
{
++i;
++j;
}
/* 指针后退重新开始匹配 */
else
{
/* j退回合适的位置,i值不变 */
j = next[j];
}
}
if (j > T[0])
return i - T[0];
else
return 0;
}
//KMP模式匹配算法改进
/* 求模式串T的next函数修正值并存入数组
nextval */
void get_nextval(String T, int *nextval)
{
int i, j;
i = 1;
j = 0;
nextval[1] = 0;
/* 此处T[0]表示串T的长度 */
while (i < T[0])
{
/* T[i]表示后缀的单个字符, */
/* T[j]表示前缀的单个字符 */
if (j == 0 || T[i] == T[j])
{
++i;
++j;
/* 若当前字符与前缀字符不同 */
if (T[i] != T[j])
/* 则当前的j为nextval在i位置的值 */
nextval[i] = j;
else
/* 如果与前缀字符相同,则将前缀 */
/* 字符的nextval值赋值给nextval在i位置的值 */
nextval[i] = nextval[j];
}
else
/* 若字符不相同,则j值回溯 */
j = nextval[j];
}
}
六、数组和广义表(线性表的拓展)
1.稀疏矩阵的存储
(一)三元组顺序表
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20
typedef struct Triple{
int row, col;
int value;
}Triple;
typedef struct TSMatrix{
Triple data[MAXSIZE + 1]; //为和矩阵对齐,下标为0的位置不使用
int mu,nu,tu; //矩阵的行数,列数和非零元素个数
}TSMatrix;
稀疏矩阵压缩存储缺点:无法根据行列号计算矩阵元素的存储地址
求矩阵的转置
方法一:按M的列序转置
//M是转置前的矩阵,T是转置后的新矩阵
//p标记原矩阵,q标记转置后的矩阵,p,q相当于两个工作指针
for (col = 1; col <= M.nu; ++ col){
for (p = 1; p <= M.tu; ++ p){ //p为转置前三元组下标,开始时下标为1
if ( M.data[p].j == 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;
++ q; //q为转置后三元组下标
}
}
}
时间复杂度:O(nu*tu),若tu与mu*nu同数量级:时间复杂度:O(nu^2*mu)
tu与mu*nu同数量级时,算法时间复杂度高 算法仅适用于 tu << mu*nu 的情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tAQHT0Jt-1646652039918)(数据结构.assets/1637059659248.png)]
方法二:按 M 的行序转置 —— 快速转置
原理:如果能预先确定矩阵M中每一列(即T中每一行)的第一个非零元在b.data中恰当位置。
那么在对a.data中的三元组一次做转置时,便可直接放到b.data中恰当的位置上去。
num[col]表示矩阵M中第col列中的非零元素个数。 //个数
cpot[col]指M中第col列的第一个非零元在b.data中的恰当位置。 //位置
cpot[0]=0;
cpot[col]=copt[col-1]+num[col-1] 1<=col<a.nu
//快速转置
void FastTransposeSMatrix(TSMatrix M, TSMatrix &T)
{
T.mu = M.nu;
T.nu = M.mu;
T.tu = M.tu;
if(T.tu)
{
int col;
int num[100], cpot[100];
for (col = 0; col < M.nu; col++)
num[col] = 0; //个数全部初始化为0
for (int t = 0; t < M.tu; t++)
++num[M.data[t].j]; //求个数
cpot[0] = 0;
for (col = 1; col < M.nu; col++)
cpot[col] = cpot[col - 1] + num[col - 1]; //求位置
int q;
for (int p = 0; 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];
}//for
}//if
return;
}//FastTransposeSMatrix
时间复杂度O(nu+tu)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uxk1g1aG-1646652039918)(数据结构.assets/1637059751309.png)]
(二)行逻辑联接的顺序表(带行表的三元组)
为便于随机存取任意一行的非零元,需知道每一行的第一个非零元在三元组表中的位置
将快速转置矩阵的算法中创建的指示“行”信息的辅助数组cpot固定在稀疏矩阵的存储结构中,这种“带行链接信息”的三元组表为行逻辑链接顺序表
typedef struct Triple{
int i, j;
int e;
}Triple;
typedef struct RLSMatrix{
Triple data[MAXSIZE+1];
int mu,nu,tu;
int rpos[MAXRC + 1]; //各行第一个非零元的位置表
}RLSMatrix;
//矩阵乘法
//1.普通矩阵乘法
for (i = 1; i <= m1; i++) //m1为行,行限定循环
{
for (j = 1; j <= n2; j++) //n2为列,列限定循环
{
Q[i][j] = 0; //初始化二维数组
for (k = 1; k <= n1; k++)
{
Q[i][j] += M[i][k] * N[K][j];//行列相乘并累加
}
}
}
(三)十字链表
typedef struct OLNode {
int i, j; //行号与列号
ElemType e; //值
struct OLNode *right, *down; //指针域
}OLNode, *OLisk;
typedef struct
{ OLink* rhead, *chead; //行、列链表的头指针向量基址
int mu, nu, tu; //稀疏矩阵的行数、列数、非零元个数
}CrossList;
十字链表的创建
int CreateSMatrix(CrossList *M)
{
int i, j, m, n, t;
int k, flag;
ElemType e;
OLNode *p, *q;
//输入稀疏矩阵的基本信息
if (M->Rhead)
DestroySMatrix(M); do {
flag = 1;
printf("输入需要创建的矩阵的行数、列数以及非零元的个数");
scanf("%d%d%d", &m, &n, &t);
if (m<0 || n<0 || t<0 || t>m*n)
flag = 0;
}while (!flag);
M->mu = m;
M->nu = n;
M->tu = t;
//创建行链表头数组
M->Rhead = (OLink *)malloc((m+1) * sizeof(OLink));
if(!M->Rhead)
exit(-1);
//创建列链表头数组
M->Chead = (OLink *)malloc((n+1) * sizeof(OLink));
if(!(M->Chead))
exit(-1);
for(k=1;k<=m;k++) // 初始化行头指针向量;各行链表为空链表
M->Rhead[k]=NULL;
for(k=1;k<=n;k++) // 初始化列头指针向量;各列链表为空链表
M->Chead[k]=NULL;
//输入各个结点
for (k=1; k<=t; ++k)
{
do {
flag = 1;
printf("输入第%d个结点行号、列号以及值", k);
scanf("%d%d%d", &i, &j, &e);
if (i<=0 || j<=0)
flag = 0;
}while (!flag); p = (OLink) malloc (sizeof(OLNode));
if (NULL == p)
exit(-1);
p->i = i;
p->j = j;
p->e = e;
//节点的行插入
if(NULL==M->Rhead[i] || M->Rhead[i]->j>j) //-----(1)
{
// p插在该行的第一个结点处
// M->Rhead[i]始终指向它的下一个结点
p->right = M->Rhead[i];
M->Rhead[i] = p;
}
else // 寻查在行表中的插入位置
{
//从该行的行链表头开始,直到找到
for(q=M->Rhead[i]; q->right && q->right->j < j; q=q->right) ; //----(2)
p->right=q->right; // 完成行插入
q->right=p;
}
//节点的列插入
if(NULL==M->Chead[j] || M->Chead[j]->i>i)
{
p->down = M->Chead[j];
M->Chead[j] = p;
}
else // 寻查在列表中的插入位置
{
//从该列的列链表头开始,直到找到
for(q=M->Chead[j]; q->down && q->down->i < i; q=q->down)
;
p->down=q->down; // 完成行插入
q->down=p;
}
} return 1;
}
2.广义表(非线性结构)
(一)基本概念
广义表是线性表的推广,已不是线性表,能描述元素间的非线性关系。
广义表(又称列表 Lists)是n≥0个元素 a1, a2, …, an的有限序列,其中每一个ai 或者是原子,或者是一个子表。
一般用大写字母表示广义表,小写字母表示原子。 LS = (a1,a2,…,an)
表头:第一个元素是表头(可以是原子,也可以是子表)head(LS) = a1
表尾:除表头之外的其它元素组成的表。(表尾一定是表) tail(LS) = (a2, …, an)
(二)性质
1.广义表中的数据元素有相对次序
2.长度:广义表的长度定义为最外层所包含元素的个数
C=(a, (b, c)) 是长度为 2 的广义表
3.深度:广义表的深度定义为该广义表展开后所含括号的重数
4.广义表可以为其他广义表共享
5.广义表可以是递归的表。 F=(a, F)=(a, (a, (a, …)))
递归表的深度是无穷值,长度是有限值。
6.广义表是多层次的,可以用图形象表示
广义表(((a,b,(),c),d),e,((f),g))的长度是__3__,深度是__4__。 长度看个数,深度看括号层数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KYV0iRi2-1646652039919)(数据结构.assets/1637422251704.png)]
(三)存储结构
1.首尾链表存储结构
typedef enum{
ATOM,LIST
//这个相当于是一个集合,ElemTag类型的元素可以等于这两个属性
}ElemTag;
typedef struct GLNode{
ElemTag tag;//区分原子结点、表结点
union{
AtomType atom;//tag==ATOM时,atom存放原子结点值
struct{//tag==LIST时,表示是子表
struct GLNode *hp;
struct GLNode *tp;
}ptr;//表结点的指针域,ptr.hp指向表头,ptr.tp指向表尾
}un;
}GLNode,*GList;//广义表
2.拓展线性链表(孩子兄弟链表)
typedef enum{ATOM,LIST}ElemTag; //ATOM == 0:原子,LIST == 1:字表
typedef struct GLNode{
ElemTag tag; //公共部分,用于区分原子节点和表节点
union share{ //原子节点和表节点的联合部分
AtomType atom; //atom是原子节点的值域,AtomTpye由用户定义
struct GLNode *hp; //表节点的表头指针
};
struct GLNode *tp; //相当于线性链表的next,指向下一个元素节点
}*GList
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pWz46LPB-1646652039920)(数据结构.assets/1637424131637.png)]
(四)各种算法
1.创建空的广义表
void InitGList(GList &L){
L = NULL;
}
2.创建原子结点
GLNode *MakeAtom(AtomType e){
GLNode *p;
p=(GList)malloc(sizeof(GLNode));//分配空间
if(p==NULL)return NULL;//分配空间失败,可能是缓存不足
p->tag=ATOM;
p->un.atom=e;
return p;
}
3.求长度
int GListLength(GList L){
int len = 0;
if(NULL==L)return 0;
if(L->tag == ATOM)
return 1;
while(L!=NULL){
L = L->un.ptr.tp;
len++;
}
return len;
}
4.获取表头
GLNode *GetHead(GList L){
GList p;
InitGList(p);
if (GListEmpty(L))return NULL;
else{
p=new GLNode;//新建一个表结点出来,在L上操作会影响到后边的操作
p->tag = L->un.ptr.hp->tag;
p->un.ptr.tp = NULL;
if (p->tag == ATOM)
p->un.atom = L->un.ptr.hp->un.atom;//把表头的原子值保存到p中
else//把L的表头保存在p的表头处
p->un.ptr.hp=L->un.ptr.hp;
return p;
}
}
5.获取表尾
GLNode *GetTail(GList L){
if(L==NULL)return NULL;
return L->un.ptr.tp;
}
GLNode *GetTail1(GList L){
GLNode *p;
if(L==NULL)return NULL;
for(p=L;p->un.ptr.tp!=NULL;p=p->un.ptr.tp);
//p的的尾指针为NULL时,p就是这个广义表的尾结点
return p;
}
6.求广义表深度
int GListDepth(GList L){
int d;
GLNode *p;
if(L==NULL)return 1;//空结点
if(L->tag==ATOM)return 0;//原子结点
for(p=L;p!=NULL;p=p->un.ptr.tp){
//有点笨的方法,本来两个递归就行,但是没成功,先用这个看看吧,也还行
d=GListDepth(p->un.ptr.hp)+1;
if(d>0)return d+1;
}
}
7.广义表的遍历
void visit(AtomType e){//简单的输出函数,如果觉得麻烦也可以不用函数
printf("%c ",e);
}
Status GListTraverse(GList &L,void(*visit)(AtomType e)){
if(L==NULL)return NULL;
if(L->tag==ATOM)visit(L->un.atom);
else{
GListTraverse(L->un.ptr.hp,visit);
GListTraverse(L->un.ptr.tp,visit);
}
return OK;
}
七、树
1.基本概念
(1)定义:树是 n(n≥0)个结点的有限集
(递归定义)满足两个条件:1.有且仅有一个称为根的结点 2.其余结点可分为m个互不相交的有限集,每个集合又是一棵树(根的子树)
树的度:最大分支树 树的深度:有几层
有序树和无序树
森林:森林是 m (m≥0) 棵互不相交的树的集合
(2)二叉树定义: n(n≥0)个结点的有限集
任何树均可与二叉树相互转换,解决了树的存储结构及其运算中存在的复杂性。
特点:
每个结点最多有两个孩子
子树有左右之分不能颠倒
二叉树可以是空集合,根可以有空的左子树或空的右子树(体现左右之分不能颠倒)
二叉树有五种基本形态
满二叉树与完全二叉树
二叉树和树的区别
二叉树不是特殊的树,二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分
树当结点只有一个孩子时,就无须区分它是左还是右,这是二者最主要的差别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wbtPyPzb-1646652039921)(数据结构.assets/1635582400990.png)](3)二叉树的五个性质
- 性质1: 在二叉树的第 i 层上至多有 2^ i- 1 个结点 (i ≥1)
性质2: 深度为 k 的二叉树至多有 2^k-1 个结点(k ≥1)
性质3: 对任何一棵二叉树 T,如果其叶子数为 n0,度为2的结点数为n2, n0 = n2 + 1
性质4: 具有 n 个结点的完全二叉树的深度为 |log2n| + 1
性质5: 对于任意结点i,求双亲|i/2| 左孩子2i 右孩子2i+1
1.树的存储结构
(一)双亲表示法(双亲角度)
/* 结点结构 */
typedef struct PTNode
{
int data; //存储数据信息
int parent; //存储该结点的双亲在数组中的下标
} PTNode;
/* 树结构 */
typedef struct
{
/* 结点数组 */
PTNode nodes[MAX_TREE_SIZE];
/* 根的位置和结点数 */
int r, n;
} PTree;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FwLeLpUO-1646652039922)(数据结构.assets/1637047417397.png)]
根结点是没有双亲的,约定根结点的位置域设置为-1
可以根据结点的parent指针很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。
可如果要知道结点的孩子是什么,要遍历整个结构才行。
改进
方便找到孩子:增加长子域,如果没有孩子的结点,这个长子域就设置为-1
注意兄弟之间的关系:增加一个右兄弟域来体现兄弟关系,没有设置为-1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cghRdePY-1646652039923)(数据结构.assets/1637048572461.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lp9h5h6Z-1646652039923)(数据结构.assets/1637048592921.png)]
(二)孩子表示法(孩子角度)
/* 孩子结点 */
typedef struct CTNode
{
int child;
struct CTNode *next;
} *ChildPtr;
/* 表头结构 */
typedef struct
{
TElemType data;
TElemType parent;
ChildPtr firstchild;
} CTBox;
/* 树结构 */
typedef struct
{
/* 结点数组 */
CTBox nodes[MAX_TREE_SIZE];
/* 根的位置和结点数 */
int r,n;
} CTree;
树中每个结点可能有多棵子树,可以考虑用多重链表,有两种设计方案
一、指针域的个数等于树的度 缺点:树中各结点的度相差很大时,很浪费空间
二、指针域个数按需分配,取一个位置来存储结点指针域的个数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KjwM4Gb3-1646652039924)(数据结构.assets/1637048997173.png)]
data为数据域,degree为度域,child1到childd为指针域
缺点:各个结点的链表是不相同的结构,加上要维护结点的度的数值,运算时间有很大的消耗
三、再次改进:孩子表示法
把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2rr3bzy7-1646652039925)(数据结构.assets/1637049249118.png)]
缺点:无法找到双亲,需要遍历整颗树
改进:双亲表示法和孩子表示法结合:双亲孩子表示法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ClVWSdz6-1646652039926)(数据结构.assets/1637049390249.png)]
(三)孩子兄弟表示法(兄弟角度)
树这样的层级结构,只研究结点的兄弟是不行的
但是任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的
优点:通过这种方法树可以转换为二叉树
缺点:无法找到双亲,但也可改进(增加双亲结点)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJTCBjQO-1646652039927)(数据结构.assets/1637049862810.png)]
typedef struct CSNode
{
TElemType data;
struct CSNode *firstchild,
*rightsib;
} CSNode, *CSTree;
2.二叉树的存储结构
(一)顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要
能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。
不存在的结点设置为“∧”
极端情况:斜二叉树,造成存储空间的大量浪费
(二)链式存储结构(二叉链表)
链式存储结构(二叉链表)
在 n 个结点的二叉链表中有 n + 1 个空指针域。
//二叉树的存储结构,一个数据域,2个指针域
typedef struct BiTNode
{
char data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//二叉树的建立(二级指针创建)
void CreateBiTree(BiTree *T)
{
char ch;
scanf("%c",&ch);
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(-1);
(*T)->data=ch;//这里是前序遍历方式创建,中序或后续改变三者顺序即可
CreateBiTree(&(*T)->lchild);
CreateBiTree(&(*T)->rchild);
}
}
不能使用一级指针的原因:
我们传的参数是一级指针。把指针p传送给pt.
此时pt和p都指向同一块内存单元。他们都指向空指针。
二这行代码pt=(char*)malloc(strlen("double shuai"+1));
为pt重新分配了一块内存单元,pt指向长度为strlen("double shuai"+1)的内存单元。
但是我们需要注意的是,p依然指向NULL.我们都知道的是,新参是局部变量。
当然退出的时候,会自动释放pt指针。但是我们就无法释放动态分配的内存单元,造成了内存单元泄露。
//二叉树的遍历:得到二叉树中所有结点的一个线性排列。
一、递归遍历
//二叉树的先序遍历
1.先序遍历根结点
2.先序遍历左子树(递归操作)
3.先序遍历右子树(递归操作)
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return ;
printf("%c ",T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
二、非递归遍历(与栈结合)
//先序遍历非递归
typedef struct TreeNode{
int data;
struct TreeNode *lChild;
struct TreeNode *rChild;
}TreeNode;
void preOrder(TreeNode *T){
TreeNode *stack[15];
int top = -1;
TreeNode *p = T;
while(p!=NULL||top!=-1){ //p不空||栈不空
if(p!=NULL){//p不空,入栈
stack[++ top] = p;
printf("%d\t",p->data); //入栈时,访问输出
p = p->lChild;
}else{
p = stack[top --];
p = p->rChild;
}
}
}
//中序遍历非递归
void inOrder(TreeNode *T){
TreeNode *stack[15];
int top = -1;
TreeNode *p = T;
while(p!=NULL||top!=-1){
if(p!=NULL){
stack[++ top] = p;
p = p->lChild;
}else{
p = stack[top --];
printf("%d\t",p->data); //出栈时,访问输出
p = p->rChild;
}
}
}
//后序遍历非递归
//需要定义一个数组记录每个节点访问次数,访问次数为1,表明是从左子树回退,不需要出栈,访问次数为2,表明是从右子树回退,需要出栈。
void postOrder(TreeNode *T){
TreeNode *stack[15];
int top = -1;
int flagStack[15]; //记录每个节点访问次数栈
TreeNode *p = T;
while(p!=NULL||top!=-1){
if(p!=NULL){ //第一次访问,flag置1,入栈
stack[++ top] = p;
flagStack[top] = 1;
p = p->lChild;
}else{//(p == NULL)
if(flagStack[top] == 1){ //第二次访问,flag置2,取栈顶元素但不出栈
p = stack[top]; //取栈
flagStack[top] = 2;
p = p->rChild;
}else{ //第三次访问,出栈
p = stack[top --];
printf("%d\t",p->data); //出栈时,访问输出
p = NULL; //p置空,以便继续退栈
}
}
}
}
//销毁
void DestroyBTree(BTree* T)
{
if (T) //双亲结点不为空
{
if (T->Lchild) //左孩子不为空
DestroyBTree(&T->Lchild); //销毁左孩子
if (T->Rchild) //右孩子不为空
DestroyBTree(&T->Rchild); //销毁右孩子
free(T); //销毁双亲结点
T = NULL; //双亲结点指空
}
}
//删除左子树
void DeLeftChild(BTree* T)
{
if (T) //双亲结点非空
DestroyBTree(&(T->Lchild)); //销毁左子树
}
//删除右子树
void DeRightChild(BTree* T)
{
if (T)
DestroyBTree(&(T)->Rchild);
}
//求深度
int MaxDepth(BTree T) {
if (T == NULL) {
return 0; //返回值传给maxLeft或者maxRight
}
else {
int maxLeft = MaxDepth(T->Lchild); //maxLeft最终值是0
int maxRight = MaxDepth(T->Rchild); //maxRight最终值也是0
if (maxLeft > maxRight) {
return 1 + maxLeft; //返回双亲结点加上最深子树的深度
}
else {
return 1 + maxRight;
}
}
}
//求所有结点个数
int NodeCount(BTree T)
{
if (T == NULL)
return 0;
else
return NodeCount(T->Lchild) + NodeCount(T->Rchild) + 1;
}
//度为1的结点个数
int Out1_Count(BTree T)
{
if (T == NULL) //空树
return 0;
if ((T->Lchild == NULL && T->Rchild != NULL) || (T->Lchild != NULL && T->Rchild == NULL)) //左右孩子有且只有一个
return Out1_Count(T->Lchild) + Out1_Count(T->Rchild) + 1; //返回双亲加上子树中度为1的结点
else
return Out1_Count(T->Lchild) + Out1_Count(T->Rchild); //左右孩子都存在,双亲结点出度为二,返回子树
}
//叶子节点个数
int LeafCount(BTree T)
{
if (T == NULL) //空树
return 0;
else if (T->Lchild ==NULL && T->Rchild == NULL) //非空树,但是无孩子
return 1;
else
return LeafCount(T->Lchild) + LeafCount(T->Rchild); //有左右孩子
}
非递归遍历二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iY78dNJR-1646652039928)(数据结构.assets/1635661778909.png)]
3.线索二叉树(利用空指针域)
n个结点的二叉链表,有n+1个空指针域。
为了区分二叉树的左孩子指针和右孩子指针是否为空,或者是否指向前驱节点或后继节点,我们将节点的结构改成5个域,在原二叉树的基础上添加左标志域Ltag和右标志域Rtag,他们是两个int型的数据域。 (适合频繁遍历二叉树 )
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWGU8N6a-1646652039929)(数据结构.assets/1635665854799.png)]
- 如果节点有左孩子,那么Lchild依然指向他的左孩子,没有左孩子就指向遍历序列中他的前驱结点
- 如果节点有右孩子,那么Rchild依然指向他的左孩子,没有右孩子就指向遍历序列中他的后继节点。
线索二叉树等于是把一棵二叉树转变成了一个双向链表
线索:指向前驱和后继节点的指针。 线索化:将空指针改为线索的过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmET9WbC-1646652039930)(数据结构.assets/1635678175833.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvSYqh8v-1646652039930)(数据结构.assets/1635678194520.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QMzjbker-1646652039931)(数据结构.assets/1635678227514.png)]
// 全局变量 prev 指针,指向刚访问过的结点
TTreeNode *prev = NULL;
/**
* 先序线索化
*/
void preorder_threading(TTreeNode *root)
{
if (root == NULL) {
return;
}
if (root->left_child == NULL) {
root->left_flag = 1;
root->left_child = prev;
}
if (prev != NULL && prev->right_child == NULL) {
prev->right_flag = 1;
prev->right_child = root;
}
prev = root;
if (root->left_flag == 0) {
preorder_threading(root->left_child);
}
if (root->right_flag == 0) {
preorder_threading(root->right_child);
}
}
4.树、二叉树、森林的转换
(1) 树转化成二叉树(兄弟相连留长子)
1.加线:在所有兄弟结点之间加一条连线。
2.去线:对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3.层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
树转换成的二叉树其右子树一定为空
(2) 二叉树转化成树(左孩右右连双亲,去掉原来右孩线)
1.加线:若 p 结点是左孩子,则将 p 的右孩子、右孩子的右孩子…沿分支找到的所有右孩子,都与 p 的双亲用线连起来。
2.去线:抹掉原二叉树中双亲与右孩子之间的连线。
3.调整:将结点按层次排列,形成树结构。
(3) 森林转化成二叉树(树变二叉根相连)
1.把每个树转换为二叉树。
2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一 棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
3.当所有的二叉树连接起来后就得 到了由森林转换来的二叉树。
(4) 二叉树转化成森林(去掉全部右孩线,孤立二叉再还原)
5.树与森林的遍历
(1)树的遍历
1.一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。
2.另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。
(2)森林的遍历
1.前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,
再依次用同样方式遍历除去第一棵树的剩余树构成的森林。
2.后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,
再依次同样方式遍历除去第一棵树的剩余树构成的森林。
森林的前序遍历和二叉树的前序遍历结果相同, 森林的后序遍历和二叉树的中序遍历结果相同。
6.哈夫曼树(最优二叉树)
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。
树的路径长度就是从树根到每一结点的路径长度之和。
考虑到带权的结点
结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。
树的带权路径长度(WPL)为树中所有叶子结点的带权路径长度之和。
哈夫曼树:带权路径长度WPL最小的二叉树称做哈夫曼树
哈夫曼树构造:
1.先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列
2.取头两个最小权值(每次取两个最小的)的结点作为一个新节点N1的两个子结点,将N1替换A与E,插入有序序列中,保持从小到大排列
3.重复2
哈夫曼树只有度为0和度为2的结点
哈夫曼编码
在哈夫曼树添加0和1,规则左0右1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Id81st4E-1646652039932)(数据结构.assets/1637054683190.png)]
编码:
A:10 B:01 C:0011 D:11 E:000 F:00101 G:00100
传送ABC时,编码为 10 01 0011
结论:出现得越多的字母,编码越短 ,出现频率越少的字母,编码越长
八、图
1.基本概念
(一)定义
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
图中数据元素称为顶点,图结构中,不允许没有顶点,边集可以是空的。
(二)各种定义
无向边:若顶点vi到vj之间的边没有方向,则称这条边为无向边,用无序偶对**(vi ,vj)**来表示。
如果图中任意两个顶点之间的边都是无向边,则称该图为无向图。
无向图由于没有方向,所以连接顶点A与D的边,可以表示成无序对(A,D),也可以写成(D,A)。
有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧。用有序偶**<Vi,Vj>(不能颠倒顺序)**来表示,vi称为弧尾,vj称为弧头。
如果图中任意两个顶点之间的边都是有向边,则称该图为有向图
简单图:在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。 (离散:没有平行边和回路)
完全无向图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。 (完全图:任意两个结点间都存在边)
含有n个顶点的无向完全图有 n(n-1)/2条边。
有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。
含有n个顶点的有向完全图有**n×(n-1)**条边
与图的边或弧相关的数叫做权 带权的图通常称为网
子图:假设有两个图G=(V,{E})和G’=(V’,{E’}),如果V’V且E’E,则称G’为G的子图
无向图
度:顶点v的度是和v相关联的边的数目,记为TD(v)
边数=各顶点度数和的一半
有向图
入度:以顶点v为头的弧的数目称为v的入度,记为ID(v)。
出度:以v为尾的弧的数目称为v的出度,记为OD(v) 。
顶点v的度为TD(v)=ID(v)+OD(v)
路径:路径的长度是路径上的边或弧的数目
第一个顶点和最后一个顶点相同的路径称为回路或环
简单路径:序列中顶点不重复出现的路径称为简单路径
(三)连通图
如果对于图中任意两个顶点vi、vj∈V,vi和vj都是连通的,则称G是连通图(对于无向图)
在有向图中,如果对于每一对vi、vj∈V、vi≠vj,从vi到vj和从vj到vi都存在路径,则称是强连通图
有向图中的极大强连通子图称做有向图的强连通分量。
无向图中的极大连通子图称为连通分量
连通分量要求:
- 要是子图
- 子图要是连通的
- 连通子图含有极大顶点数
- 具有极大顶点数的连通子图包含依附于这些顶点的所有边
生成树:所有顶点均由边连接在一起但不存在回路的图。(n个顶点n-1条边且连通)
生成树是图的极小联通子图,去掉一条边则非联通
(遍历时生成)构造生成树:深度优先生成树、广度优先生成树
有向树:如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一个有向树。
生成森林:对于非连通图,其每个连通分量可以构造一棵生成树,合成起来就是一个生成森林。
有向图的生成森林:由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。
(四)总结
图按方向分:
无向图(由顶点和边构成)
有向图(有向图由顶点和弧构成。弧有弧尾和弧头之分 )
图按边(弧)多少分:
稀疏图
稠密图
图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度,有向图顶点分为入度和出度。图上的边或弧上带权则称为网
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则
图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。
无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。
2.图的抽象数据类型
图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系。
所以图不能用简单的顺序存储结构表示
(一)邻接矩阵(数组与数组结合)
用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
无向图样例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bsxoxC4y-1646652039932)(数据结构.assets/1635507740805.png)]
等于1意味边存在,等于0意味着边不存在
无向图的边数组是一个对称矩阵
顶点的度等于这个顶点在邻接矩阵中第i行(或第i列)的元素之和
有向图样例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6k7zvzoj-1646652039933)(数据结构.assets/1635508753714.png)]
入度是第v1列各数之和 出度是第v1列各行数之和
加权值的邻接矩阵
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NFAGWbk8-1646652039934)(数据结构.assets/1635509054356.png)]
/* 最大顶点数,应由用户定义 */
#define MAXVEX 100
/* 用65535来代表∞ */
#define INFINITY 65535
typedef struct
{
//顶点表
VertexType vexs[MAXVEX];
//邻接矩阵,可看作边表
EdgeType arc[MAXVEX][MAXVEX];
//图中当前的顶点数和边数
int numVertexes, numEdges;
} MGraph;
创建无向网图
void CreateMGraph(MGraph *G)
{
int i, j, k, w;
//输入顶点数和边数
scanf("%d,%d", &G->numVertexes, &G->numEdges);
//建立顶点表
for (i = 0; i < G->numVertexes; i++){
scanf(&G->vexs[i]);
}
for (i = 0; i < G->numVertexes; i++)
for (j = 0; j <G->numVertexes; j++)
//邻接矩阵初始化
G->arc[i][j] = INFINITY;
//读入numEdges条边,建立邻接矩阵
for (k = 0; k < G->numEdges; k++)
{
printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
scanf("%d,%d,%d", &i, &j, &w);
G->arc[i][j] = w;
//无向图,矩阵对称
G->arc[j][i] = G->arc[i][j];
}
}
(二)邻接表(数组与链表结合)
邻接矩阵对于边数相对顶点较少的图,是存在对存储空间的极大浪费的
因此考虑另外一种存储结构方式。在线性表时,顺序存储结构就存在预先分配内存可能造成存储空间浪费的问题,于是引出了链式存储的结构。同样,我们也可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。
//用邻接矩阵表示一个图是唯一的,用邻接表表示是不唯一的
无向图邻接表结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-axJxIN2s-1646652039935)(数据结构.assets/1635510892411.png)]
顶点的度:顶点的边表中结点的个数
要判断顶点vi到vj是否存在边:测试顶点vi的边表中是否存在结点vj的下标
有向图邻接表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bxl7Fcln-1646652039935)(数据结构.assets/1635511127322.png)]
以顶点为弧尾来存储边表,可以得到每个顶点的出度
有向图逆邻接表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXKHoJTj-1646652039936)(数据结构.assets/1635511213313.png)]
以顶点为弧头来存储边表,可以得到每个顶点的入度
带权值的网图,可以在边表结点定义中再增加一个的数据域,存储权值信息
//结点的定义
typedef struct EdgeNode
{
int adjvex; //下标
EdgeType weight; //权值,非网图不需要
struct EdgeNode *next;
} EdgeNode;
/* 顶点表结点 */
typedef struct VertexNode
{
VertexType data; //顶点
EdgeNode *firstedge; //边表头指针
} VertexNode, AdjList[MAXVEX]; //AdjList = VertexNode[MAXVEX]
typedef struct
{
AdjList adjList;
int numVertexes, numEdges; //图中当前顶点数和边数
} GraphAdjList;
//无向图邻接表的创建
void CreateALGraph(GraphAdjList *G)
{
int i, j, k;
EdgeNode *e;
scanf("%d,%d", &G->numVertexes, &G->numEdges);
for (i = 0; i < G->numVertexes; i++)
{
scanf(&G->adjList[i].data);
G->adjList[i].firstedge = NULL;
}
for (k = 0; k < G->numEdges; k++)
{
// 输入边(vi,vj)上的顶点序号
scanf("%d,%d", &i, &j);
//向内存申请空间
//生成边表结点
e = (EdgeNode *)malloc(sizeof(EdgeNode));
//邻接序号为j
e->adjvex = j;
//将e指针指向当前顶点指向的结点
e->next = G->adjList[i].firstedge;
//将当前顶点的指针指向e
G->adjList[i].firstedge = e;
//向内存申请空间
//生成边表结点
e = (EdgeNode *)malloc(sizeof(EdgeNode));
//邻接序号为i
e->adjvex = i;
//将e指针指向当前顶点指向的结点
e->next = G->adjList[j].firstedge;
//将当前顶点的指针指向e
G->adjList[j].firstedge = e;
}
}
(三)十字链表(存放有向图)
对于有向图来说,邻接表是有缺陷的,不能同时关心入度和出度
把邻接表与逆邻接表结合——十字链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nrIX53lt-1646652039936)(数据结构.assets/1636000801959.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfownz2L-1646652039937)(数据结构.assets/1636000818942.png)]
(四)邻接多重表(存放无向图)
删除一条边需要删除边表的两个结点,比较繁琐,仿照十字链表对边表进行改造
改造后的边表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zAUTc35h-1646652039938)(数据结构.assets/1636012829903.png)]
ivex和jvex是与某条边依附的两个顶点在顶点表中的下标。
ilink指向依附顶点ivex的下一条边
jlink指 向依附顶点jvex的下一条边
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Oe2mT2q-1646652039939)(数据结构.assets/1636012798070.png)]
3.图的遍历
(一)深度优先遍历
遍历实质:找到每个结点的邻接点
对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。
避免重复访问:设置辅助数组visit[n],用来标记每个被访问过的结点
初始visit[i]=0, 访问过变为1
邻接矩阵遍历
//Boolean是布尔类型,其值是TRUE或FALSE
typedef int Boolean;
//辅助数组
Boolean visited[MAX];
//邻接矩阵的深度优先递归算法
void DFS(MGraph G, int i)
{
int j;
visited[i] = TRUE;
//打印顶点
printf("%c ", G.vexs[i]);
for (j = 0; j < G.numVertexes; j++)
if (G.arc[i][j] == 1 && !visited[j])
//对为访问的邻接顶点递归调用
DFS(G, j);
}
/* 邻接矩阵的深度遍历操作 */
void DFSTraverse(MGraph G)
{
int i;
for (i = 0; i < G.numVertexes; i++)
//初始所有顶点状态都是未访问过状态
visited[i] = FALSE;
for (i = 0; i < G.numVertexes; i++)
//对未访问过的顶点调用DFS,若是连通图,只会执行一次
if (!visited[i])
DFS(G, i);
}
邻接表遍历
//邻接表的深度优先递归算法
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
//打印顶点
printf("%c ", GL->adjList[i].data);
p = GL->adjList[i].firstedge;
while (p)
{
if (!visited[p->adjvex])
/* 对为访问的邻接顶点递归调用 */
DFS(GL, p->adjvex);
p = p->next;
}
}
/* 邻接表的深度遍历操作 */
void DFSTraverse(GraphAdjList GL)
{
int i;
for (i = 0; i < GL->numVertexes; i++)
/* 初始所有顶点状态都是未访问过状态 */
visited[i] = FALSE;
for (i = 0; i < GL->numVertexes; i++)
/* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
if (!visited[i])
DFS(GL, i);
}
(二)广度优先遍历
如果说深度优先遍历是相当于树的前序遍历,那么,广度优先遍历就相当于树的层序遍历。
邻接矩阵遍历
邻接表遍历
4.最小生成树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DZPIF0S7-1646652039940)(数据结构.assets/1638519776837.png)]
给定一个无向网络,在该网的所有生成树中,使得个边权值最小的那棵生成树称为该网的最小生成树。
(一)MST性质
设N = (V,E)是一个连通网,U是顶点集V的一个非空子集。若边(u,v)是一条具有最小权值的边,u属于U,v属于V-U,则必存在在一棵包含(u,v)的最小生成树。
性质解释:
在生成树的构造过程中,图中n个顶点分为两个集合:
已落在生成树上的顶点集 U
尚未落在生成树上的顶点集 V-U
接下来则应该在所有连通U中顶点和V-U中顶点的边中选取权值最小的边
(二)Prim算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lyni0pgB-1646652039941)(数据结构.assets/1636082215854.png)]
(三)Kruskal算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DVn3NUbV-1646652039941)(数据结构.assets/1636102610703.png)]
Prim算法 | Kruskal算法 |
---|---|
算法思想:选择点 | 算法思想:选择边 |
时间复杂度:n^2(n为顶点数) | 时间复杂度:eloge(e为边数) |
适用范围:稠密图 | 适用范围:稀疏图 |
5.最短路径
在有向图中A点(源点)到达B点(终点)的多条路径中,找到一条各边权值之和最小的路径,即最短路径
最短路径与最小生成树不同,路径上不一定包括n个顶点,也不一定包含n-1个边
(一)单源最短路径(迪杰斯特拉(Dijkstra)算法)
按路径长度递增次序产生最短路径
1.初始化:先找出从源点V0到各终点Vk的直达路径(V0,VK),就是通过一条弧到达的路径,没有用无穷大表示
2.选择:从这些路径中找出一条长度最短的路径(V0,U0)
3.更新:然后对其余各路径进行适当调整[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pyHLQDHA-1646652039942)(数据结构.assets/1636104445842.png)]
两部分加起来比直达的路径短,用经过U0点的路径代替原路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oRN32VCb-1646652039942)(数据结构.assets/1636105860291.png)]
时间复杂度O(n^2)
(二)所有顶点间的最短路径(弗洛伊德(Floyd)算法)
方法一:n个顶点用n次Dijkstra算法 时间复杂度O(n^3)
6.拓扑排序
(一)AOV网(顶点表示活动)
有向无环图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,称为AOV网
AOV网不能存在回路:某个活动的开始要以自己完成作为先决条件,显然是不可以的
拓扑排序:
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2,……,vn,满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj 之前。则我们称这样的顶点序列为一个拓扑序列。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PDZWHNFu-1646652039943)(数据结构.assets/1636276707548.png)]
判断AOV网或者有向无环图:看所有顶点都在拓扑排序内
(二)拓扑排序算法
思路:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删 除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。
实质:对有向图的顶点排成一个线性序列。
7.关键路径
(一)AOE网(边表示活动)
AOE网是一个带权的有向无环图,其中顶点表示事件,弧表示活动,权表示活动持续的时间。
AOE网中入度为零的点称为源点,将出度为零的点称为汇点
完成工程的最短时间是从源点到汇点的最长路径的长度。路径长度最长的路径就叫做 关键路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8ccoo4X-1646652039944)(数据结构.assets/1636277305654.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JE5gaQFS-1646652039945)(数据结构.assets/1636277373140.png)]活动的最早开工时间就是活动发生前的事件的最早开始时间
活动的最晚发生时间则是基于事件的最晚发生时间。 最晚发生时间减去活动所需时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kaL5QLYj-1646652039946)(数据结构.assets/1636277525108.png)]
九 查找
1.基本概念
查找表(为查找定义的数据结构)
由同一类型的数据元素(或记录)构成的集合。 由于“集合”之中的数据元素之间存在比较松散的关系,所以查找表是一种比较灵便的应用
经常操作:查找、检索、增加、删除
分类:静态查找表(仅限前两种) 动态查找表
区别:
静态查找是平时概念上的查找,在静态查找中仅仅是执行“查找”的操作
动态查找首先也有查找的过程,但多了插入和删除操作
关键字:数据元素中某个数据项的值,用以标识一个数据元素,如果是唯一标识,则称为主关键字。 用以识别若干记录的关键字是次关键字
是否查找成功:根据给定的值,在查找表中确定一个其关键字等于给定值的元素,如果表中存在这样元素,则称查找成功,否则,不成功。
注:查找的方法取决于查找表的结构,本章的出发点是设法提高查找表的查找效率
提高查找效率,一个方法就是在集合内的数据元素之间人为的加上约束关系
查找算法评价标准
ASL 关键字平均比较次数,也称平均查找长度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sQuasmyt-1646652039946)(数据结构.assets/1636379887275.png)]
n:记录的个数
pi:查找第i个记录的概率(pi=1/n)
ci:找到第i个记录所需要的比较次数
2.查找方式
从逻辑上来说,查找所基于的数据结构是集合,集合中的记录之间没有本质关系。可是要想获得较高的查找性能,我们就不能不改变数据元素之间的关系,在存储时可以将查找集合组织成表、树等结构。
(一)静态查找
(1)顺序表查找
定义:从表的一端(头或尾)开始,逐个进行记录的关键字和给定值的比较。
适用范围:1.顺序表或者线性链表表示的静态查找表 2.表内元素无序
结构体数组 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AurzGnxq-1646652039947)(数据结构.assets/1636382567707.png)]
顺序查找算法
/* 顺序查找,a为数组,n为要查找的数组长度,
key为要查找的关键字 */
int Sequential_Search(int *a, int n, int key)
{
int i;
for (i = 1; i <= n; i++)
{
if (a[i] == key)
return i;
}
return 0;
}
顺序查找算法优化
顺序查找算法要判断两次,是否找到关键字,数组下标是否越界
设置一个哨兵,可以解决不需要每次让i与n作比较
查找不成功时,关键字的比较次数总是 n+1次
/* 有哨兵顺序查找 */
int Sequential_Search2(int *a, int n, int key)
{
int i;
/* 设置a[0]为关键字值,我们称之为“哨兵” */
a[0] = key;
/* 循环从数组尾部开始 */
i = n;
while (a[i] != key)
{
i--;
}
/* 返回0则说明查找失败 */
return i;
}
再次改进
查找概率的不同,可以将容易查找到的记录放在前面,而不常用的记录放置在后面,效率就可以有大幅提高。
(2)有序表查找(折半)
折半查找又称二分查找。它的前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储
非递归算法
int Search(SeqList R, int n, KeyType k)
{
int low, high, mid;
low = 1;
high = n;
while (low <= high)
{
mid = (low + high) / 2; //整除,不是整数向下取整
if (k < R[mid].Key)
high = mid - 1;
else if (k > R[mid].Key)
low = mid + 1;
else
return mid;
}
return 0;
}
递归算法
#include <stdio.h>
#define MAXL 100
typedef int KeyType;
typedef char InfoType[10];
typedef struct
{
KeyType key; //KeyType为关键字的数据类型
InfoType data; //其他数据
} NodeType;
typedef NodeType SeqList[MAXL]; //顺序表类型
int BinSearch1(SeqList R,int low,int high,KeyType k)
{
int mid;
if (low<=high) //查找区间存在一个及以上元素
{
mid=(low+high)/2; //求中间位置
if (R[mid].key==k) //查找成功返回其逻辑序号mid+1
return mid+1;
if (R[mid].key>k) //在R[low..mid-1]中递归查找
BinSearch1(R,low,mid-1,k);
else //在R[mid+1..high]中递归查找
BinSearch1(R,mid+1,high,k);
}
else
return 0;
}
折半查找性能分析—判定树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SyX7s4h9-1646652039948)(数据结构.assets/1636431830140.png)]
ASL:log2(n+1)-1
(3)有序表查找(分块)
条件 1.将表分成几块,且表或者有序,或者分块有序;若i<j,则第j块中所有记录的关键字均大于第i块中的最大关键字。
2.建立"索引表”(每个结点含有最大关键字域和指向本块第一个结点的指针,且按关键字有序)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rDb0Qaa2-1646652039948)(数据结构.assets/1636432798655.png)]
索引表内折半或者顺序查找
块内顺序查找
查找效率:ASL= Lb + Lw 对索引表查找的ASL+对块内查找的ASL
顺序查找、折半查找、分块查找都是静态查找
(二)动态查找
(1)树表的查找
当表插入、删除操作太频繁时,为了维护表的有序性,需要移动表中的很多记录
解决方法:改用动态查找——几种特殊的树
1.二叉排序树(二叉查找树)
二叉排序树既有类似于折半查找的特性,又采用了链表作存储结构。
(1)若其左子树非空,则左子树上所有结点的值均小于根结点的值
(2)若其右子树非空,则右子树上所有结点的值均大于等于根结点的值
(3)其左右子树本身又各是一棵二叉排序树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0qRjHfFH-1646652039949)(数据结构.assets/1636554792563.png)]
中序遍历二叉排序树:3 12 24 37 45 53 61 78 90 100
中序遍历非空二叉排序树的关键字顺序为递增有序顺序
查找:若查找的关键字等于根结点查找成功,否则若小于根结点,查其左子树,若大于根结点,查其右子树,在左右子树上操作类似
二叉排序树的存储结构
typedef struct {
KeyType key;//关键字项
InfoType otherinfo;//其他数据域
} ElemType;
typedef struct BSTNode {
ElemType data; //数据域
struct BSTNode *Ichild, *rchild;
//左右孩子指针
} BSTNode, *BSTree;
算法思想
(1) 若二叉排序树为空,则查找失败,返回空指针。
(2)若二叉排序树非空,将给定值key与根结点的关键字T-> data.key进行比较:
①若key等于T->data.key,则查找成功,返回根结点地址;
②若key小于T-> data.key,则进一步查找左子树;
③若key大于T->data.key,则进-步查找右子树。
查找分析
含有n个结点的二叉排序树的平均查找长度和树的形态有关
最好情况
初始序列{45,24,53,12,37,93}
ASL=log 2(n + 1)-1 树的深度为: Llog 2n」+ 1(向上取整)
与折半查找中的判定树相同。 (形态比较均衡) : 0 (log2n)
最坏情况
初始序列{12,24,37,45,53,93} 插入的n个元素从一开始就有序——变成单支树的形态
此时树的深度为n, ASL=(n+1)/2
查找效率与顺序查找情况相同O(n)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HVpZeEkb-1646652039950)(数据结构.assets/1636719617105.png)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8MkqgWk-1646652039950)(数据结构.assets/1636719658630.png)]
插入
若二叉排序树为空,则插入结点作为根结点插入到空树中
否则,继续在其左、右子树上查找
- 树中已有,不再插入
- 树中没有:查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子
插入的元素一定是在叶子结点上
插入的结点均为叶子结点,故无需移动其他结点。相当于在有序序列上插入记录而无需移动其他记录
生成:关键字的输入顺序不同,建立的不同二叉排序树
删除
从二叉排序树中删除一个结点, 不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变。由于中序遍历二叉排序树可以得到一个递增有序的序列。那么,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点。
- 将因删除结点而断开的二叉链表重新链接起来
- 防止重新链接后树的高度增加
(1)被删除的结点是叶子结点:直接删去该结点,其双亲结点中相应指针域的值改为“空”
(2)被删除的结点只有左子树或者只有右子树,用其左子树或者右子树替换(结点替换)
其双亲结点的相应指针域的值改为“指向被删除结点的左子树或右子树”
- 以其中序前趋值替换之(值替换),然后再删除该前趋结点。前趋是左子树中最大的结点。
- 也可以用其后继替换之,然后再删除该后继结点。后继是右子树中最小的结点。
提高形态不均衡的二叉排序树查找效率?
做“平衡化”处理,尽量让二叉树的形状均衡
(2)平衡二叉树
平衡二叉树(AVL树)
一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树
- ①左子树与右子树的高度之差的绝对值小于等于1
- ②左子树和右子树也是平衡二叉排序树。
给每个结点附加一个数字,给出该结点左子树与右子树的高度差。这个数字称为结点的平衡因子(BF) 。
平衡因子=结点左子树的高度—结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是-1、0,或1
失衡二叉排序树的分析和调整
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Agw5Pj1E-1646652039951)(数据结构.assets/1636724121782.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5E7uuS7-1646652039951)(数据结构.assets/1636724147356.png)]
调整原则:(1)降低高度 (2)保持二叉排序树性质
代码
//定义平衡二叉树结点
typedef struct Node
{
int key;
struct Node *left;
struct Node *right;
int height;
}BTNode;
//LL型调整
BTNode *ll_rotate(BTNode *y)
{
BTNode *x = y->left;
y->left = x->right;
x->right = y;
y->height = max(height(y->left), height(y->right)) + 1;
x->height = max(height(x->left), height(x->right)) + 1;
return x;
}
//RR型调整
BTNode *rr_rotate(struct Node *y)
{
BTNode *x = y->right;
y->right = x->left;
x->left = y;
y->height = max(height(y->left), height(y->right)) + 1;
x->height = max(height(x->left), height(x->right)) + 1;
return x;
}
(3)B树
(三)哈希查找
哈希表(散列表)
以上的各种查找结构都有共同特点:记录在表中的位置和它的关键字之间不存在一个确定的关系。
对于频繁使用的查找表,希望 ASL = 0
解决办法:记录在表中的位置和其关键字之间存在一种确定的(函数)关系。
哈希函数:将关键字的集合映射到某个地址集合上
冲突:key1!=key2,而 f (key1) = f (key2) 有相同函数值的关键字叫同义词
哈希表查找关注的两大问题:1.构造哈希函数 2.处理冲突
构造哈希函数
1.直接定址法:哈希函数为关键字的线性函数
H(key) = key 或者 H(key) = a *key + b (调整a与b解决冲突)
特点:地址集合的大小等于关键字的大小
2.数字分析法(数字选择法):取关键字的若干位或其组合作哈希地址。
特点:适于关键字位数比哈希地址位数大,且可能出现的关键字事先知道的情况。
举例:有 80 个记录,关键字为 8 位十进制数,哈希表长为 100。则哈希地址可取2位十进制数。
3.平方取中法:以关键字的平方值的中间几位作为哈希地址。
特点:关键字中的每一位都有某些数字重复出现频度很高的现象。
开平方后既能扩大差距,又能保持与之前关键字的联系
4.折叠法:将关键字分割成位数相同的几部分,然后取这几部分的叠加和(舍去进位)做哈希地址
特点:适于关键字位数很多,且每一位上数字分布大致均匀情况
5.除留取余法(最常用)
取关键字被某个不大于哈希表表长 m 的数 p除后所得余数作哈希地址
即 H(key) = key MOD p, p<=m p的选取对冲突的影响很大
6.随机数法:取关键字的随机函数值作哈希地址
H(key) = Random(key)
处理冲突
1.开放定址法
当发生冲突时,在冲突位置的前后附近寻找可以存放记录的空闲单元。
需要一个探测序列,沿着序列去寻找可以存放记录的空闲单元
探查地址序列 H0, H1, H2, …, Hs n
Hi = ( H(key) + di ) MOD m i =1, 2, …, s
沿此序列逐个地址探查,直到找到一个空位置(开放的地址), 将发生冲突的记录放到该地址中。
对增量di的三种取法
- 线性探测再散列:di = 1, 2, 3, …, m-1
- 二次探测再散列(平方探测再散列 ):di = 1², -1², 2², -2², 3², …, ±k²
- 伪随机探测再散列(双散列函数探测再散列 ): di = 伪随机数序列
2.再哈希法
方法:构造若干个哈希函数,当发生冲突时,计算另一个哈希地址
3.溢出区法
方法:除基本的存储区外,另外建立一个公共溢出区,当发生冲突时,记录可以存入这个公共溢出区。
4.链地址法
方法:将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放头指针
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N1eZbZl8-1646652039952)(数据结构.assets/1638100989418.png)]
十 排序
1.基本概念
定义:将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列
排序的稳定性:如果两记录Ri与Rj的关键字相同,在排序前Ri在Rj的前面,排序之后, Ri依然在Rj之前,我们称这种排序方法是稳定的,反之,是不稳定的。
2.排序方法
(一)插入排序类
(1)直接插入排序
采取顺序查找法查找插入位置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sm1ITdCQ-1646652039953)(数据结构.assets/1638102562223.png)]
步骤:1.顺序查找法找到插入位置
2.记录后移
3.插入
过程:先将序列中第 1 个记录看成是一个有序子序列,然后从第 2 个记录开始,逐个进行插入,直至整个序列有序。
最好情况:关键字在记录中顺序有序 最坏情况:关键字在记录中逆序有序
时间复杂度:T(n)=O(n²) 空间复杂度:S(n)=O(1)
是稳定排序
(2)折半插入排序
用折半查找方法确定插入位置的排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vpfbWyRL-1646652039953)(数据结构.assets/1638103064453.png)]
时间复杂度:T(n)=O(n²) 空间复杂度:S(n)=O(1)
仅减少了比较次数, 移动次数不变。
是稳定排序
(3)缩小增量排序(希尔排序)
希尔排序的出发点:增加比较后移动的部分
之前:比较一次,移动一步 改进:比较一次,移动一大步
基本思想:先将整个待排记录分割成若干子序列,分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插 入排序
特点:1.缩小增量 2.多遍插入排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f4xvUOna-1646652039954)(数据结构.assets/1638103793637.png)]
- 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
最后一次只需要少量移动
增量序列必须是递减的, 最后一个必须是1
增量序列应该是互质的
时间复杂度约为:O(n1.3) 打破是排序时间复杂度不能小于n^2的局面
希尔排序是不稳定的
(二)交换排序类
基本思想:两两比较,如果逆序就交换,直到排好为止
(1)冒泡排序
优点:稳定
缺点:慢,每次只能移动两个相邻的数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZSdA8bz-1646652039954)(数据结构.assets/1638104501344.png)]
n个记录,要比较n-1次
第m趟比较n-m次
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PdcIR83P-1646652039955)(数据结构.assets/1638105041839.png)]
时间复杂度:T(n)=O(n²) 空间复杂度:S(n)=O(1)
冒泡排序是稳定的
(2)快速排序
改进的交换排序
基本思想:1.任取一个元素(如第一个)为中心
2.所有比它小的一律前放,比它大的后放,形成左右两个子表
3.对左右子表再次重复1,2
4.直到每个子表的元素只剩一个
优点:极快数据移动少 缺点:不稳定
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vPPl72qJ-1646652039956)(数据结构.assets/1638105624205.png)]
每一趟的子表的形成是采用从两头向中间交替式逼近法
由于每趟中对各子表的操作都相似,可采用递归算法。
时间复杂度:O(nlogn) 就平均计算时间而言,快速排序是所有排序内方法最好的一个
空间复杂度:快速排序不是原地排序,使用了递归,需要栈的支持 O(logn)
快速排序的效率在序列越乱的时候,效率越高。在数据有序时,会退化成冒泡排序
(三)选择排序类
(1)简单选择排序
排序过程:1.首先通过 n –1 次关键字比较,从 n 个记录中找出关键字最小的记录,将它与第一个记录交换。
2.再通过 n–2 次比较,从剩余的 n –1 个记录中找出关键字次小的记录,将它与第二个记录交换
3.重复上述操作,共进行 n –1 趟排序后,排序结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8O6IUBMd-1646652039956)(数据结构.assets/1638153549550.png)]
(2)堆排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vqFaNT2v-1646652039959)(数据结构.assets/1638153630187.png)]
排序过程:1.将无序序列建成一个堆,得到关键字最小(大)的记录;
2.输出堆顶的最小(大)值后,将剩余的 n-1 个元素重又建成一个堆,则可得到 n 个元素的次小值;
3.如此重复执行,直到堆中只有一个记录为止,每个记录出堆的顺序就是一个有序序列,这个过程叫堆排序。
关注问题:1.无序序列如何建成堆 2.输出堆顶元素后,调整其他元素变成新的堆
(四)其他排序
(1)归并排序
2路归并排序:将两个位置相邻的记录有序子序列归并为一个记录有序的序列。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uue4DorS-1646652039960)(数据结构.assets/1638153999039.png)]
(2)基数排序
基数排序不需要进行元素的比较与交换,基数排序适合于有不同位数的大小数字
核心思想:
1.先找十个桶:0~9 第一轮按照元素的个位数排序 桶内分别存放上述数组元素的个位数,按照数组元素的顺序依次存放
2.之后,按照从左向右,从上到下的顺序依次取出元素,组成新的数组。
3.在新的数组中,进行第二轮,按照十位数排序,依次存放于桶中
4.按照之前的顺序取出,组成新的数组,进行第三轮,按照百位数排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9dm2T1UX-1646652039960)(数据结构.assets/1638154498040.png)]
(五)总结
排序名称 | 时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
直接插入排序 | T(n)=O(n²) | S(n)=O(1) | 稳定 |
折半插入排序 | T(n)=O(n²) | S(n)=O(1) | 稳定 |
希尔排序 | T(n) =O(n1.3) | S(n)=O(1) | 不稳定 |
冒泡排序 | T(n)=O(n²) | S(n)=O(1) | 稳定 |
快速排序 | T(n)=O(nlogn) | S(n)=O(logn) | 不稳定 |
简单选择排序 | T(n)=O(n²) | S(n)=O(1) | 不稳定 |
堆排序 | T(n)=O(nlogn) | S(n)=O(1) | 不稳定 |
归并排序 | T(n)=O(nlogn) | S(n)=O(n) | 稳定 |
基数排序 | T(n)=O(n) | S(n)=O(rd) | 稳定 |
:将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列
排序的稳定性:如果两记录Ri与Rj的关键字相同,在排序前Ri在Rj的前面,排序之后, Ri依然在Rj之前,我们称这种排序方法是稳定的,反之,是不稳定的。
2.排序方法
(一)插入排序类
(1)直接插入排序
采取顺序查找法查找插入位置
[外链图片转存中…(img-Sm1ITdCQ-1646652039953)]
步骤:1.顺序查找法找到插入位置
2.记录后移
3.插入
过程:先将序列中第 1 个记录看成是一个有序子序列,然后从第 2 个记录开始,逐个进行插入,直至整个序列有序。
最好情况:关键字在记录中顺序有序 最坏情况:关键字在记录中逆序有序
时间复杂度:T(n)=O(n²) 空间复杂度:S(n)=O(1)
是稳定排序
(2)折半插入排序
用折半查找方法确定插入位置的排序
[外链图片转存中…(img-vpfbWyRL-1646652039953)]
时间复杂度:T(n)=O(n²) 空间复杂度:S(n)=O(1)
仅减少了比较次数, 移动次数不变。
是稳定排序
(3)缩小增量排序(希尔排序)
希尔排序的出发点:增加比较后移动的部分
之前:比较一次,移动一步 改进:比较一次,移动一大步
基本思想:先将整个待排记录分割成若干子序列,分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插 入排序
特点:1.缩小增量 2.多遍插入排序
[外链图片转存中…(img-f4xvUOna-1646652039954)]
- 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
最后一次只需要少量移动
增量序列必须是递减的, 最后一个必须是1
增量序列应该是互质的
时间复杂度约为:O(n1.3) 打破是排序时间复杂度不能小于n^2的局面
希尔排序是不稳定的
(二)交换排序类
基本思想:两两比较,如果逆序就交换,直到排好为止
(1)冒泡排序
优点:稳定
缺点:慢,每次只能移动两个相邻的数据
[外链图片转存中…(img-lZSdA8bz-1646652039954)]
n个记录,要比较n-1次
第m趟比较n-m次
[外链图片转存中…(img-PdcIR83P-1646652039955)]
时间复杂度:T(n)=O(n²) 空间复杂度:S(n)=O(1)
冒泡排序是稳定的
(2)快速排序
改进的交换排序
基本思想:1.任取一个元素(如第一个)为中心
2.所有比它小的一律前放,比它大的后放,形成左右两个子表
3.对左右子表再次重复1,2
4.直到每个子表的元素只剩一个
优点:极快数据移动少 缺点:不稳定
[外链图片转存中…(img-vPPl72qJ-1646652039956)]
每一趟的子表的形成是采用从两头向中间交替式逼近法
由于每趟中对各子表的操作都相似,可采用递归算法。
时间复杂度:O(nlogn) 就平均计算时间而言,快速排序是所有排序内方法最好的一个
空间复杂度:快速排序不是原地排序,使用了递归,需要栈的支持 O(logn)
快速排序的效率在序列越乱的时候,效率越高。在数据有序时,会退化成冒泡排序
(三)选择排序类
(1)简单选择排序
排序过程:1.首先通过 n –1 次关键字比较,从 n 个记录中找出关键字最小的记录,将它与第一个记录交换。
2.再通过 n–2 次比较,从剩余的 n –1 个记录中找出关键字次小的记录,将它与第二个记录交换
3.重复上述操作,共进行 n –1 趟排序后,排序结束
[外链图片转存中…(img-8O6IUBMd-1646652039956)]
(2)堆排序
[外链图片转存中…(img-vqFaNT2v-1646652039959)]
排序过程:1.将无序序列建成一个堆,得到关键字最小(大)的记录;
2.输出堆顶的最小(大)值后,将剩余的 n-1 个元素重又建成一个堆,则可得到 n 个元素的次小值;
3.如此重复执行,直到堆中只有一个记录为止,每个记录出堆的顺序就是一个有序序列,这个过程叫堆排序。
关注问题:1.无序序列如何建成堆 2.输出堆顶元素后,调整其他元素变成新的堆
(四)其他排序
(1)归并排序
2路归并排序:将两个位置相邻的记录有序子序列归并为一个记录有序的序列。
[外链图片转存中…(img-Uue4DorS-1646652039960)]
(2)基数排序
基数排序不需要进行元素的比较与交换,基数排序适合于有不同位数的大小数字
核心思想:
1.先找十个桶:0~9 第一轮按照元素的个位数排序 桶内分别存放上述数组元素的个位数,按照数组元素的顺序依次存放
2.之后,按照从左向右,从上到下的顺序依次取出元素,组成新的数组。
3.在新的数组中,进行第二轮,按照十位数排序,依次存放于桶中
4.按照之前的顺序取出,组成新的数组,进行第三轮,按照百位数排序
[外链图片转存中…(img-9dm2T1UX-1646652039960)]
(五)总结
排序名称 | 时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
直接插入排序 | T(n)=O(n²) | S(n)=O(1) | 稳定 |
折半插入排序 | T(n)=O(n²) | S(n)=O(1) | 稳定 |
希尔排序 | T(n) =O(n1.3) | S(n)=O(1) | 不稳定 |
冒泡排序 | T(n)=O(n²) | S(n)=O(1) | 稳定 |
快速排序 | T(n)=O(nlogn) | S(n)=O(logn) | 不稳定 |
简单选择排序 | T(n)=O(n²) | S(n)=O(1) | 不稳定 |
堆排序 | T(n)=O(nlogn) | S(n)=O(1) | 不稳定 |
归并排序 | T(n)=O(nlogn) | S(n)=O(n) | 稳定 |
基数排序 | T(n)=O(n) | S(n)=O(rd) | 稳定 |