数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别并输入给计算机处理的符号集合
数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理.也被称为记录.
数据项:一个数据元素可以由若干个数据项组成
数据项是数据不可分割的最小单位
数据对象:是性质相同的数据元素的集合,是数据的子集
逻辑结构:是指数据对象中数据元素之间的相互关系
物理结构:是指数据的逻辑结构在计算机中的存储形式
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称
C语言中数据类型可以分两类
抽象是指抽取出事物具有的普遍性的本质
算法具有五个基本特性:输入、输出、有穷性、确定性和可行性
好的算法,应该具有正确性、读性、健壮性、高效率和低存储量的特征
例如某个算法I的运行时间为2n²+3n+1,则首先保留最高阶项为2n²,由于最高阶项存在且不是1,所以去除与n²相乘的常数2,得到这个算法的时间复杂度为n²
int i;
for(i=0;i<n;i++)
{
//时间复杂度为O(1)的程序步骤序列
}
//因此整个循环结构运行次数为n次,即时间复杂度为O(n)
int count =1;
while(count<n)
{
count=count*2;
//时间复杂度为O(1)的程序步骤序列
}
//由于每次count乘以2之后,就距离循环退出条件n更近了一分
//由2ᕽ=n得到x=log₂n,因此整个循环的时间复杂度为O(logn)
int i,j;
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
{
//时间复杂度为O(1)的程序步骤序列
}
}
/*这是一个循环嵌套的例子,他的内循环在上面已经分析过,时间复杂度为O(n)
而对于外层的循环,不过是内部这个时间复杂度为O(n)的语句再循环n次,所以这段代码的时间复杂度为O(n²)*/
int i,j;
for(i=0;i<m;i++)
{
for(j=0;j<n;j++)
{
//时间复杂度为O(1)的程序步骤序列
}
}
//如果外循环的循环次数改成m,时间复杂度就变为O(M×N)
通过以上两个例子总结,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数
方法调用同理,只不过是将代码语句封装进了方法中
O(1)
例
红框部分为一个数据元素,而"张三"为一个数据项
/*
ADT 线性表(List)
Data
线性表的数据对象集合为{a₁,a₂,……,aₙ},每个元素的类型均为DataType.其中,除第一个元素a₁外,每一个元素有且只有直接前驱元素,除了最后一个元素aₙ外,每一个元素有且只有一个直接后继元素.数据元素之间的关系是一对一的关系
*/
Operation
InitList(*L): //初始化操作,建立一个空的线性表L
ListEmpty(L): //若线性表为空,返回true,否则返回false
ClearList(*L): //将线性表清空。
GetElem(L,i,*e): //将线性表L中的第i个位置元素值返回给e。
LocateElem(L,e): //在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。
ListInsert(*L,i,e): //在线性表L中的第i个位置插入新元素e。
ListDelete(*L,i,*e): //删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L): //返回线性表L的元素个数。
endADT
对于实际问题中涉及到的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现
可以利用C语言的一维数组来实现顺序存储结构
#define MAXSIZE 20 //存储空间初始分配量
typedef int ElemType; //ElemType类型根据实际情况而定,这里我们用C语言的typedef关键字,假设ElemType为int
typedef struct
{
ElemType data[MAXSIZE]; //数组存储数据元素,最大值为MAXSIZE
int length; //用该变量记录线性表当前长度
}SqList;
通过上述代码,可以发现描述顺序存储结构需要三个属性:
我们通过C语言的结构体关键字struct将其构造成一种类型,由上述三个成员组成
例如:一个int数组(int类型占4个字节)下标为0的数据元素的地址为11,则其下标为4的数据元素的地址为11+4*4=27
直接上C语言代码
#define OK 1
#define ERROR 0
typedef int Status;
/*Status是数据结构中抽象出来的函数的类型,其值是函数结果的状态代码,如OK等*/
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(SqList L,int i,ElemType *e)
{
if(L.length==0||i<1||i>L.length) //如果该顺序线性表为空表或传入的i值不在正常范围中
return ERROR;
*e=L.data[i-1];
return OK;
}
Status是一个抽象的函数返回值类型,我们在C语言中将其定义为int.程序可以通过Status的返回值来判断算法执行的正确与否,返回OK代表1,ERROR代表0.之后的代码中不再定义
插入算法的思路:
代码如下
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if(L->length==MAXSIZE) //顺序表满
return ERROR;
if(i<1||i<L->length+1) //当i不在范围内时
return ERROR;
if(i<=L->length) //若插入数据位置不在表尾
{
for(k=L->length-1;k>=i-1;k--) //将要插入的位置后面的数据元素都向后移动一位
L->data[k+1]=L->data[k];
}
L->data[i-1]=e; //将新元素插入
L->length++; //表长加1
return OK;
}
删除算法的思路:
代码如下
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
if(L->length==0) //线性表为空
return ERROR;
if(i<1||i>L->length) //删除位置不正确
return ERROR;
*e=L->data[i-1]; //将要删除的元素值赋给e
if(i<L->length) //如果删除不是最后位置
{
for(k=i;i<L->length;k++) //将删除位置后继元素前移
L->data[k-1]=L->data[k];
}
L->length--; //表长减1
return OK;
}
线性表插入和删除的时间复杂度都为O(n)
为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置).我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域.指针域中存储的信息称作指针或链.这两部分信息组成数据元素ai的存储映像,称为结点
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表
链表中第一个结点的存储位置叫做头指针,整个链表的存取必须是从头指针开始进行
链表中最后一个结点的指针域为"空"(通常用NULL或符号"^"表示)
//线性表的单链表存储结构
typedef struct Node
{
int data;
struct Node* next;
}Node; //定义结点结构
typedef struct Node* LinkList; //定义LinkList,某种意义上也即链表的头指针
从代码中的结构定义可知:结点由存放数据的数据域、存放后继结点地址的指针域组成
获得链表第i个数据的算法思路:
实现代码算法如下:
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L, int i, int* e)
{
int j; //声明计数器j
LinkList p; //声明一结点p
p = L->next; //让p指向链表的第一个节点
j = 1;
while (p && j < i) //p不为空或者计数器j还没有等于i时,循环继续
{
p = p->next; //p指向下一个节点
++j;
}
if (!p || j > 1)
return ERROR; //第一个元素不存在
*e = p->data; //取第i个元素的数据
return OK;
}
单链表的读取时间复杂度为O(n)
主要核心思想:运用"工作指针后移"来控制循环
核心代码: s->next=p->next; p->next=s; 顺序不能更改!!!
单链表第i个数据插入结点的算法思路:
实现代码算法如下:
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status GetElem(LinkList *L, int i, int e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while (p&&j<1) //寻找第i个结点
{
p = p->next;
++j;
}
if (!p || j > i)
return ERROR; //第i个元素不存在
s = (LinkList)malloc(sizeof(Node)); //生成新结点
s->data = e;
s->next = p->next; //将p的后继结点赋给s的后继
p->next = s; //将s赋值给p的后继
return OK;
}
C语言的标准函数malloc作用:生成一个新的结点,其类型与Node是一样的,其实质就是在内存中找了一小块空地,准备用来存放s结点
核心代码: q=p->next; p->next=q->next;
单链表第i个数据删除结点的算法思路:
实现代码算法如下:
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(LinkList* L, int i, int* e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while (p->next&&j<i)
{
p = p->next;
++j;
}
if (!(p->next) || j > i)
{
return ERROR; //第i个元素不存在
}
q = p->next;
p->next = q->next; //将q的后继赋值给p的后继
*e = q->data; //将q结点中的数据给e
free(q); //让系统回收此结点,释放内存
return OK;
}
C语言的标准函数free作用:让系统回收一个Node结点,释放内存
单链表的插入和删除的时间复杂度都是O(n)
单链表整表创建的算法思路:
实现代码算法如下:
/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList* L, int n)
{
LinkList p;
int i;
srand(time(0)); //初始化随机数种子
*L = (LinkList)malloc(sizeof(Node));//链表头空间
(*L)->next = NULL; //建立一个带头结点的单链表
for (i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node)); //生成新结点
p->data = rand() % 100 + 1; //随机生成100以内的数字
p->next = (*L)->next;
(*L)->next = p; //插入到表头
}
}
/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList* L, int n)
{
LinkList p, r;
int i;
srand(time(0)); //初始化随机数种子
*L = (LinkList)malloc(sizeof(Node)); //为整个线性表
r = *L; //r为指向尾部的结点
for (i = 0; i < n; i++)
{
p = (Node*)malloc(sizeof(Node)); //生成新结点
p->data = rand() % 100 + 1; //随机生成100以内的数字
r->next = p; //将表尾终端结点的指针指向新结点
r = p; //将当前的新结点定义为表尾终端结点
}
r->next = NULL; //尾结点的指针域置空
}
单链表整表删除的算法思路如下:
实现代码算法如下:
/*初始条件:顺序线性表L已存在,操作结果:将L重置为空表*/
Status ClearList(LinkList* L)
{
LinkList p, q;
p = (*L)->next; //p指向第一个结点
while (p) //没到表尾
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; //头结点指针域为空
return OK;
}
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域
再将双向链表优化一下,使其成为双向循环链表
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
int data;
struct DuLNode* prior; //直接前驱指针
struct DuLNode* next; //直接后继指针
}DulNode, *DuLinkList;
双向链表的一个结点如图所示
每个结点的后继的前驱、前驱的后继都是结点本身
在插入和删除时,双向链表的操作与单链表有些许不同,需要更改两个指针变量
插入
s->prior=p; //把p赋值给s的前驱,如图中1
s->next=p->next; //把p->next赋值给s的后继,如图中2
p->next->prior=s; //把s赋值给p->next的前驱,如图中3
p->next=s; //把s赋值给p的后继,如图中4
删除
p->prior->next=p->next; //把p->next赋值给p->prior的后继,如图中1
p->next->prior=p->prior;//把p->prior赋值给p->next的前驱,如图中2
free(p); //释放结点p
在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以
规律:
ADT 栈(stack)
data
同线性表.元素具有相同的类型,相邻元素具有前驱和后继关系
Operation
InitStack(*S):初始化操作,建立一个空栈S
DestroyStack(*S):若栈存在,则销毁它
ClearStack(*S):将栈清空
StackEmpty(S):若栈为空,返回true,否则返回false
GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素
Push(*S,*e):若栈S存在,插入新元素e到栈S中并成为栈顶元素
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值
StackLength(S):返回栈S的元素个数
endADT
typedef int SElemType; //SElemType类型根据实际情况而定,这里假设为int
typedef struct
{
SElemType data[MAXSIZE];
int top; //用于栈顶指针
}SqStack
对于进栈操作push,代码如下
/*插入元素e为新的栈顶元素*/
Status Push(SqStack* S, SElemType e)
{
if (S->top == MAXSIZE - 1) //栈满
{
return ERROR;
}
S->top++; //栈顶指针增加一
S->data[S->top] = e; //将新插入元素赋值给栈顶空间
return OK;
}
对于出栈操作pop,代码如下
//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
Status Pop(SqStack* S, SElemType *e)
{
if (S->top == 0)
{
return ERROR;
}
*e = S->data[S->top];
S->top--;
return OK;
}
顺序栈的进栈和出栈操作的**时间复杂度均是O(1)*
链栈的结构代码如下
typedef struct StackNode
{
SElemType data;
struct StackNode* next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
假设元素值为e的新结点是s,top为栈顶指针,进栈代码如下
//插入元素e为新的栈顶元素
Status Push(LinkStack* S, SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = S->top; //把当前的栈顶元素赋值给新结点的直接后继
S->top = s; //将新的结点s赋值给栈顶指针
S->count++;
return OK;
}
出栈操作:假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可
//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
Status Pop(LinkStack* S, SElemType* e)
{
LinkStackPtr p;
if (StackEmpty(*S))
return ERROR;
*e = S->top->data;
p = S->top;
S->top = S->top->next;
free(p);
S->count--;
return OK;
}
链栈的进栈和出栈操作时间复杂度均为O(1)
如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈
如果用常规的迭代方法(假设打印前40位)
int main()
{
int i;
int a[40];
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()
{
int i;
for(int i=0;i<40;i++)
printf("%d",Fbi(i));
return 0;
}
ADT 队列(Queue)
Data
同线性表.元素具有相同的类型,相邻元素具有前驱和后继关系
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q
DestroyQueue(*Q):若队列Q存在,则销毁它
ClearQueue(*Q):将队列Q清空
QueueEmpty(Q):若队列Q为空,返回true,否则返回false
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值
QueueLength(Q):返回队列Q的元素个数
endADT
我们将队列头尾相接,变为一个环状的空间,称为循环队列
循环队列解决了"假溢出",但没有解决真溢出(空间溢出)
在这种情况下,有两种区别队满还是队空的方法
如图所示,我们就认为此队列已经满了
循环队列的顺序存储结构代码如下
//循环队列的顺序存储结构
typedef struct
{
QElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;
循环队列的初始化代码如下
//初始化空队列Q
Status InitQueue(SqQueue* Q)
{
Q->front = 0;
Q->rear = 0;
return OK;
}
循环队列求队列长度代码如下
//返回Q的元素个数,也就是队列的当前长度
int QueueLength(SqQueue Q)
{
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
循环队列的入队列操作代码如下
//若队列未满,则插入元素e为Q的新队元素
Status EnQueue(SqQueue* Q, QElemType e)
{
if ((Q->rear + 1) % MAXSIZE == Q->front) //队列满的判断
return ERROR;
Q->data[Q->rear] = e; //将元素e赋值给队尾
Q->rear = (Q->rear + 1) % MAXSIZE; //rear指针向后移一位置
//若到最后则转到数组头部
return OK;
}
循环队列的出队列操作代码如下
Status DeQueue(SqQueue* Q, QElemType* e)
{
if (Q->front == Q->rear) //队列空的判断
return ERROR;
*e = Q->data[Q->front]; //将队头元素赋值给e
Q->front = (Q->front + 1) % MAXSIZE; //front指针向后移一位置
//若到最后则转到数组头部
return OK;
}
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列
空队列时
//队列的链结构
typedef int QElemType; //QElemType类型根据实际情况而定,这里假设为int
typedef struct QNode //结点结构
{
QElemType data;
struct QNode* next;
}QNode,*QueuePtr;
typedef struct //队列的链表结构
{
QueuePtr front, rear; //队头,队尾指针
}LinkQueue;
//插入元素e为Q的新的队尾元素
Status EnQueue(LinkQueue* Q, QElemType e)
{
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if (!s)
exit(OVERFLOW); //exit是c++程序的退出函数
//OVERFLOW为math.h的一个宏定义,其值为3
//含义为运算过程中出现了上溢,即运算结果
//超出了运算变量所能存储的范围
s->data = e;
s->next = NULL;
Q->rear->next = s; //把拥有元素e的新结点s赋值给原队尾结点的后继
Q->rear = s; //把当前的s设置为队尾结点,rear指向s
return OK;
}
//若队列不空,删除Q的队头元素,用e返回其值,并返回0,否则返回-1
Status DeQueue(LinkQueue* Q, QElemType* e)
{
QueuePtr p;
if (Q->front == Q->rear)
return ERROR;
p = Q->front->next; //将欲删除的队头结点暂存给p
*e = p->data; //将欲删除的队头结点的值赋值给e
Q->front->next = p->next; //将原队头结点后继p->next赋值给头结点后继
if (Q->rear == p) //若队头是队尾,则删除后将rear指向头结点
Q->rear = Q->front;
free(p);
return OK;
}
循环队列与链队列的时间复杂度均为O(1),但链队列每次申请和释放结点会多存在一些时间开销
在空间上,循环队列可能有存储元素个数和空间浪费的问题,而链队列不存在这个问题
ADT 串(string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系
Operation
StrAssign(T,*chars):生成一个其值等于字符串常量chars的串T
StrCopy(T,S):串S存在,由串S复制得串T
ClearString(S):串S存在,将串清空
StringEmpty(S):若串S为空,返回true,否则返回false
StrLength(S):返回串S的元素个数,即串的长度
StrCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S<T,返回值<0
Concat(T,S1,S2):用T返回由S1和S2联接而成的新串
SubString(Sub,S,pos,len):串S存在,1≤pos≤StrLength(S),且0≤len≤StrLength(S)-pos+1,用Sub返回串S的第pos个字符起长度为len的子串
Index(S,T,pos):串S和T存在,T是非空串,1≤pos≤StrLength(S).若主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0
Replace(S,T,V):串S、T和V存在,T是非空串.用V替换主串S中出现的所有与T相等的不重叠的子串
StrInsert(S,pos,T):串S和T存在,1≤pos≤StrLength(S)+1.在串S的第pos个字符之前插入串T.
StrDelete(S,pos,len):串S存在,1≤pos≤StrLength(S)-len+1.从串S中删除第pos个字符起长度为len的子串.
endADT
举例:在一篇文章中寻找一个单词.这种子串的定位操作通常称作串的模式匹配
假设要从主串S="goodgoogle"中找到T="google"这个子串的位置.
代码实现如下
//返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0
//T非空,1≤pos≤StrLength(S)
int Index(string S, string T, int pos)
{
int i = pos; //i用于主串S中当前位置下标,若pos不为1
//则从pos位置开始匹配
int j = 1; //j用于子串T中当前位置下标值
while (i <= S[0] && j <= T[0]) //若i小于S长度且j小于T的长度时循环
{
if (S[i] == T[j]) //两字母相等则继续
{
++i;
++j;
}
else //指针后退重新开始匹配
{
i = i - j + 2; //i退回到上次匹配首位的下一位
j = 1; //j退回到子串T的首位
}
}
if (j > T[0])
{
return i - T[0];
}
else
return 0;
}
BF算法较为低效
树(Tree)是n(n≥0)个结点的有限集.n=0时称为空树.在任意一棵非空树中:
树结构:
ADT 树(tree)
Data
树是由一个根结点和若干颗子树构成.树中结点具有相同数据类型及层次关系
Operation
InitTree(*T):构造空树T
DestroyTree(*T):销毁树T
CreateTree(*T,definition):按definition中给出的树的定义来构造树
ClearTree(*T):若树T存在,则将树T清为空树
TreeEmpty(T):若T为空树,返回true,否则返回false
TreeDepth(T):返回T的深度
Root(T):返回T的根结点
Value(T,cur_e):cur_e是树T中的一个结点,返回此结点的值
Assign(T,cur_e,value):给树T的结点cur_e赋值为value
Parent(T,cur_e):若cur_e是树T的非根结点,则返回他的双亲,否则返回空
LeftChild(T,cur_e):若cur_e是树T的非叶结点,则返回他的最左孩子,否则返回空
RightSibling(T,cur_e):若cur_e有右兄弟,则返回他的右兄弟,否则返回空
InsertChild(*T,*p,i,c):其中p指向树T的某个结点,i为所指结点p的度加上1,非空树c与T不相交,操作结果为插入c为树T中p指结点的第i颗子树
DeleteChild(*T,*p,i):其中p指向树T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点的第i棵子树
endADT
其中data是数据域,存储结点的数据信息.而parent是指针域,存储该结点的双亲在数组中的下标
/* 树的双亲表示法结点结构定义 */
#define MAX_TREE_SIZE 100
typedef int TElemType; //树结点的数据类型,目前暂定为整型
typedef struct PTNode //结点结构
{
TElemType data; //结点数据
int parent; //双亲位置
}PTNode;
typedef struct //树结构
{
PTNode nodes[MAX_TREE_SIZE];//结点数组
int r,n; //根的位置和结点数
}PTree;
由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1
树结构和树双亲表示所示如图
双亲表示法查找双亲结点的时间复杂度为O(1),查找结点的孩子则需要遍历整个结构
由于树中每个结点可能有多颗子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法.
此时链表中的结点有两种表示方案
另外一种办法:把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空.然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如图所示
任意一棵树,他的结点的第一个孩子如果存在就是唯一的,他的右兄弟如果存在也是唯一的.因此,我们设置两个指针,分别指向该结点的第一个孩子和此节点的右兄弟
其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址
结构定义代码如下
/* 树的孩子兄弟表示法结构定义 */
typedef struct CSNode
{
TElemType data;
struct CSNode *firstchild,*rightsib;
}CSNode,*CSTree;
好处:将一棵复杂的树变成了一棵二叉树
结合图片理解
其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针
二叉链表结点结构定义代码
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode //结点结构
{
TElemType data; //结点数据
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
1.前序遍历
/* 二叉树的前序遍历算法 */
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data); //显示结点数据,可以更改为对其他结点操作
PreOrderTraverse(T->lchild); //再先序遍历左子树
PreOrderTraverse(T->rchild); //最后先序遍历右子树
}
/* 二叉树的中序遍历算法 */
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild); //中序遍历左子树
printf("%c",T->data); //显示结点数据,可以更改为对其他结点操作
InOrderTraverse(T->child); //最后中序遍历右子树
}
/* 二叉树的后序遍历算法 */
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); //先后序遍历左子树
PostOrderTraverse()
}
为了能让每个结点确认是否有左右孩子,将二叉树中每个结点的空指针引出一个虚节点,其值为一特定值,比如"#".我们称这种处理后的二叉树为原二叉树的扩展二叉树.
假设二叉树的结点均为一个字符.把刚才前序遍历序列AB#D##C##用键盘挨个输入.实现的算法如下
/* 按前序输入二叉树中结点的值(一个字符)*/
/* #表示空树,构造二叉链表表示二叉树T*/
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c",&ch);
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; //生成根结点
CreateBiTree(&(*T)->lchild); //构造左子树
CreateBiTree(&(*T)->rchild); //构造右子树
}
}
对于n个结点的二叉树,则在二叉链存储结构中就会有n+1个空链域;对于一个有 n 个结点的二叉链表,每个结点指向左右孩子的两个指针域,故共有 2n 个指针域,而 n 个结点的二叉树共有 n-1 条分支,即存在 2n-(n-1) = n+1 个空链域
如果一个结点左孩子为空,则指向它的前驱结点,如果一个结点右孩子为空,则指向他的后继
这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树
对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化
其中:
/* 二叉树的二叉线索存储结构定义 */
typedef enum{Link,Thread}PointerTag; //Link==0 表示指向左右孩子指针
//Thread==1 表示指向前驱或后继的线索
typedef struct BiThrNode //二叉线索存储结点结构
{
TElemType data; //结点数据
struct BiThrNode *lchild,*rchild; //左右孩子指针
PointerTag LTag;
PointerTag RTag; //左右标志
}BiThrNode,*BiTherTree;
BiThrTree pre; //全局变量,始终指向刚刚访问过的结点
/* 中序遍历进行中序线索化 */
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); //递归左子树线索化
if(!p->lchild) //没有左孩子
{
p->LTag=Thread; //前驱线索
p->lchild=pre; //左孩子指针指向前驱
}
if(!pre->rchild) //前驱没有右孩子
{
pre->RTag=Thread; //后继线索
pre->rchild=p; //前驱右孩子指针指向后继(当前结点p)
}
pre=p; //保持pre指向p的前驱
InThreading(p->rchild); //递归右子树线索化
}
}
有了线索二叉树,对其遍历时其实就等于操作一个双向链表结构
遍历的代码如下:
/* T指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点.中序遍历二叉线索链表表示的二叉树T */
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p=T->lchild; // p指向根结点
while(p!=T) //空树或遍历结束时,p==T
{
while(p->LTag==Link) //当LTag==0时循环到中序序列第一个结点
p=p->lchild;
printf("%c",p->data); //显示结点数据,可以更改为对其他结点操作
while(p->RTag==Thread&&p->rchild!=T)
{
p=p->rchild;
printf("%c",p->data);
}
p=p->rchld; //p进至其右子树根
}
return OK;
}
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择
1.从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除,知道所有右孩子连线都删除为止,得到分离的二叉树
判断一棵二叉树能够转换成一棵树还是森林,只要看这棵二叉树的根结点有没有右孩子
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合
Operation
CreateGraph(*G,V,VR):按照顶点集V和边弧集VR的定义构造图G
DestroyGraph(*G):图G存在则销毁
LocateVex(G,u):若图中存在顶点u,则返回图中的位置
GetVex(G,v):返回图G中顶点v的值
PutVex(G,v,value):将图G中顶点v赋值value
FirstAdjVex(G,*v):返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点返回空
NextAdjVex(G,v,*w):返回顶点v相对于顶点w的下一个邻接顶点,若w是v的最后一个邻接点则返回空
InsertVex(*G,v):在图G中增添新顶点v
DeleteVex(*G,v):删除图G中顶点v及其相关的弧
InsertArc(*G,v,w):在图G中增添弧<v,w>,若G是无向图,还需要增添对称弧<w,v>
DeleteArc(*G,v,w):在图G中删除弧<v,w>,若G是无向图,则还需要删除对称弧<w,v>
DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程对每个顶点调用
HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用
endADT
图的邻接矩阵:图的邻接矩阵存储方式是用两个数组来表示图.一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息
arc[i] [j]=1表示从vi到vj的边存在
由图也可知无向图的边数组是一个对称矩阵,主对角线上的值为0
某个顶点的度也就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和.比如顶点v1的度就是1+0+1+0=2
求某个顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i] [j]为1就是邻接点
图的邻接矩阵存储的结构代码
typedef char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
#define MAXVEX 100 //最大顶点数,应由用户定义
#define INFINITY 65535 //用65535来代表∞
typedef struct
{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作边表
int numVertexes,numEdges; //图中当前的顶点数和边数
}MGraph;
无向网图的创建代码
/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
int i,j,k,w;
printf("输入顶点数和边数:\n");
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=0lj<G->numVertexes;j++)
G->arc[i][j]=INFINITY; //邻接矩阵初始化
}
for(k=0;k<G->numEdges;k++) //读入numEdges条边,建立邻接矩阵
{
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]; //因为是无向图,矩阵对称
}
在图的存储中,我们把数组和链表相结合的存储方法称为邻接表
为了方便确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表
/* 关于结点定义的代码 */
typedef char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
typedef struct EdgeNode //边表结点
{
int adjvex; //邻接点域,存储该顶点对应的下标
EdgeType weight; //用于存储权值,对于非网图可以不需要
struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode,AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; //图中当前顶点数和边数
}GraphAdjList;
/* 建立图的邻接表结构 */
void CreateALGraph(GraphAdjList *G)
{
int i,j,k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
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++) //建立边表
{
printf("输入边(vi,vj)上的顶点序号:\n");
scanf("%d,%d",&i,&j); //输入边上的顶点序号
e=(EdgeNode *)malloc(sizeof(EdgeNode)); //向内存申请空间生成边表结点
e->adjvex=j; //邻接序号为j
e->next=G->adjList[i].firstedge; //将e指针指向当前顶点指向的顶点
G->adjList[i].firstedge=e; //将当前顶点的指针指向e
e=(EdgeNode *)malloc(sizeof(EdgeNode)); //向内存申请空间生成边表结点
e->adjvex=i; //邻接序号为i
e->next=G->adjList[j].firstedge; //将e指针指向当前顶点指向的结点
G->adjList[j].firstedge=e; //将当前顶点的指针指向e
}
}
对于n个顶点e条边来说,时间复杂度为O(n+e)
深度优先遍历(Depth_First_Search),也称为深度优先搜索,简称为DFS
深度优先遍历是从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到.若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止
邻接矩阵结构代码如下
typedef int Boolean; //Boolean是布尔类型,其值是TRUE或FALSE
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++)
if(!visited[i]) //对未访问过的结点调用DFS,若是连通图,只会执行一次
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++)
if(!visited[i]) //对未访问过的顶点调用DFS,若是连通图,只会执行一次
DFS(GL,i);
}
对于n个顶点e条边的图来说,邻接矩阵时间复杂度O(n²),邻接表时间复杂度O(n+e)
广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称BFS.
广度优先遍历是从图的某一结点出发,首先依次访问该结点的所有邻接顶点vi1,vi2,…vin再按这些顶点被访问的先后次序依次访问与他们相邻接的所有未被访问的顶点,重复此过程直至所有顶点均被访问为止
/* 邻接矩阵的广度遍历算法 */
void BFSTraverse(MGraph.G)
{
int i,j;
Queue Q;
for(i=0;i<G.numVertexes;i++)
visited[i]=FALSE;
InitQueue(&Q); //初始化一辅助用的队列
for(i=0;i<G.numVertexes;i++) //对每一个顶点做循环
{
if(!visited[i]) //若是未访问过就处理
{
visited[i]=TRUE; //设置当前顶点访问过
printf("%c",G.Vexs[i]); //打印顶点,也可以其他操作
EnQueue(&Q,i); //将此顶点入队列
while(!QueueEmpty(Q)) //若当前队列不为空
{
DeQueue(&Q,&i); //将队中元素出队列,赋值给i
for(j=0;j<G.numVertexes;j++)
{
//判断其他顶点若与当前顶点存在边且未访问过
if(G.arc[i][j]==1&&!visited[j])
{
visited[j]=TRUE; //将找到的此顶点标记为已访问
printf("%c",G.vexs[j]); //打印顶点
EnQueue(&Q,j); //将找到的此顶点入队列
}
}
}
}
}
}
/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for(i=0;i<GL->numVertexes;i++)
visited[i]=FALSE;
InitQueue(&Q);
for(i=0;i<GL->numVertexes;i++)
{
if(!visited[i])
{
visited[i]=TRUE;
printf("%c",GL->adjList[i].data); //打印顶点,也可以其他操作
EnQueue(&Q,i);
while(!QueueEmpty(Q))
{
DeQueue(&Q,&i);
p=GL->adjList[i].firstedge; //找到当前顶点边表链表头指针
while(p)
{
if(!visited[p->adjvex]) //若此顶点未被访问
{
visited[p->adjvex]=TRUE;
printf("%c",GL->adjList[p->adjvex].data);
EnQueue(&Q,p->adjvex); //将此顶点入队列
}
p=p->next; //指针指向下一个邻接点
}
}
}
}
}
首先随意找一个顶点,一般都是v1,然后找到与v1相连的权值最小的边(若有多条,则随机选取一条),然后找与这两个点相连的有最小权值的边,然后与找这三个点相连权值最小的边,一直重复,直到有n-1条边,并且连接完所有的顶点,注意,不能形成环。
算法时间复杂度为O(n²),更适合稠密图
基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路。
具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。
时间复杂度为:O(eloge) 边数e越大,所耗费时间越长,则适合稀疏图
算法思想:
Dijkstra(迪杰斯特拉)算法理解_哔哩哔哩_bilibili
时间复杂度O(n³)
/* 顺序查找,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;
}
/* 有哨兵顺序查找 */
int Sequential_Search2(int *a,int n,int key)
{
int i;
a[0]=key; //设置a[0]为关键字值,我们称之为哨兵
i=n; //循环从数组尾部开始
while(a[i]!=key)
{
i--;
}
return i; //返回0则说明查找失败
}
折半查找又称为二分查找.它的前提是线性表中的记录必须是关键码有序(通常是从小到大有序),线性表必须采用顺序存储.折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找.不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止.
/* 折半查找 */
int Binary_Search(int *a,int n,int key)
{
int low,high,mid;
low=1; //定义最低下标为记录首位
high=n; //定义最高下标为记录末位
while(low<=high)
{
mid=(low+high)/2; //折半
if(key<a[mid]) //若查找值比中值小
high=mid-1; //最高下标调整到中位下标小一位
else if(key>a[mid]) //若查找值比中值大
low=mid+1; //最低下标调整到中位下标大一位
else
return mid; //若相等则说明mid即为查找到的位置
}
return 0;
}
折半算法的时间复杂度为O(logn),但前提条件是需要有序表顺序存储
折半查找在查找不成功和给定值进行比较的关键字个数最多为[log2n]+1
分块查找又称为索引顺序查找,这是一种性能介于顺序查找和折半查找之间的一种查找方法
分块查找,又称为索引顺序查找,吸取了顺序查找和折半查找各自的优点,既有动态结构,又适于快速查找
基本思想:将查找表分为若干个子块,块内元素可以无序,块间元素有序
块间有序含义: 若a
建立“索引表”,每个结点含有最大关键字域和指向本块第一个结点的指针,且按关键字有序
优缺点:
二叉排序树:二叉排序树又称二叉查找树,它具有以下性质:
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode //结点结构
{
int data; //结点数据
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
/* 递归查找二叉排序树T中是否存在key,
指针f指向T的双亲,其初始调用值为NULL
若查找成功,则指针p指向该数据元素结点,并返回TRUE
否则指针p指向查找路径上访问的最后一个结点并返回FALSE */
Status SearchBST(BiTree T,int key,BiTree f,BiTree *p)
{
if(!T) //查找不成功
{
*p=f;
return FALSE;
}
else if(key==T->data) //查找成功
{
*p=T;
return TRUE;
}
else if(key<T->data)
{
return SearchBST(T->lchild,key,T,p); //在左子树继续查找
}
else
return SearchBST(T->rchild,key,T,p); //在右子树继续查找
}
平衡二叉树是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1
它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。若将二叉树节点的平衡因子BF定义为该节点的左子树的深度减去它的右子树的深度,则平衡二叉树上所有节点的平衡因子只可能为-1,0,1.
只要二叉树上有一个节点的平衡因子的绝对值大于1,那么这颗平衡二叉树就失去了平衡。
平衡二叉树的生成炒鸡简单不用LR、RL(以王道2021数据结构平衡二叉树的生成过程为例)_哔哩哔哩_bilibili
直接插入排序是一种最简单的排序方法,其基本操作是将一条记录插入到已排好序的有序表中.从而得到一个新的、记录数量增1的有序表
/* 对顺序表做直接插入排序 */
void InsertSort(SqList *L)
{
int i,j;
for(i=2;i<=L->length;i++)
{
if(L->r[i]<L->r[i-1]) //需将L->r[i]插入有序子表
{
L->r[0]=L->r[i]; //设置哨兵
for(j=i-1;L->r[j]>L->r[0];j--)
L->r[j+1]=L->r[j]; //记录后移
L->r[j+1]=L->r[0]; //插入到正确位置
}
}
}
时间复杂度O(n²),空间复杂度O(1)
特点:
/* 对顺序表L做冒泡排序 */
void BubbleSort(SqList *L)
{
int i,j;
for(i=1;i<L->length;i++)
{
for(j=L->length-1;j>=i;j--) //注意j是从后往前循环
{
if(L->r[j]>L->r[j+1]) //若前者大于后者
{
swap(L,j,j+1); //交换L->r[j]与L->r[j+1]的值
}
}
}
}
/* 对顺序表L作改进冒泡算法 */
void BubbleSort2(SqList *L)
{
int i,j;
Status flag=TRUE; //flag用来作为标记
for(i=1;i<L->length&&flag;i++) //若flag为true则退出循环
{
flag=FALSE; //初始为false
for(j=L->length-1;j>=i;j--)
{
if(L->r[j]>L->r[j+1])
{
swap(L,j,j+1); //交换L->r[j]与L->r[j+1]的值
flag=TRUE; //如果有数据交换,则flag为true
}
}
}
}
/* 可以避免因已经有序的情况下的无意义循环判断 */
简单选择排序法就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之
/* 对顺序表L作简单选择排序 */
void SelectSort(SqList *L)
{
int i,j,min;
for(i=1;i<L->length;i++)
{
min=i; //将当前下标定义为最小值下标
for(j=i+1;j<=L->length;j++) //循环之后的数据
{
if(L->r[min]>L->r[j]) //如果有小于当前最小值的关键字
min=j; //将此关键字的下标赋值给min
}
if(i!=min) //若min不等于i,说明找到最小值,交换
swap(L,i,min); //交换L->r[i]与L->r[min]的值
}
}
时间复杂度:O(n²),空间复杂度:O(1)
特点:
基数排序是从最低位(个位)开始进行,按关键字的不同收集,然后按第二低位(十位)开始进行,按关键字的不同收集,若有些数没有更低位,则按0收集
特点:
)
{
int i,j;
for(i=1;ilength;i++)
{
for(j=L->length-1;j>=i;j–) //注意j是从后往前循环
{
if(L->r[j]>L->r[j+1]) //若前者大于后者
{
swap(L,j,j+1); //交换L->r[j]与L->r[j+1]的值
}
}
}
}
- ```c
/* 对顺序表L作改进冒泡算法 */
void BubbleSort2(SqList *L)
{
int i,j;
Status flag=TRUE; //flag用来作为标记
for(i=1;ilength&&flag;i++) //若flag为true则退出循环
{
flag=FALSE; //初始为false
for(j=L->length-1;j>=i;j--)
{
if(L->r[j]>L->r[j+1])
{
swap(L,j,j+1); //交换L->r[j]与L->r[j+1]的值
flag=TRUE; //如果有数据交换,则flag为true
}
}
}
}
/* 可以避免因已经有序的情况下的无意义循环判断 */
简单选择排序法就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之
/* 对顺序表L作简单选择排序 */
void SelectSort(SqList *L)
{
int i,j,min;
for(i=1;i<L->length;i++)
{
min=i; //将当前下标定义为最小值下标
for(j=i+1;j<=L->length;j++) //循环之后的数据
{
if(L->r[min]>L->r[j]) //如果有小于当前最小值的关键字
min=j; //将此关键字的下标赋值给min
}
if(i!=min) //若min不等于i,说明找到最小值,交换
swap(L,i,min); //交换L->r[i]与L->r[min]的值
}
}
时间复杂度:O(n²),空间复杂度:O(1)
特点:
基数排序是从最低位(个位)开始进行,按关键字的不同收集,然后按第二低位(十位)开始进行,按关键字的不同收集,若有些数没有更低位,则按0收集
特点:
[外链图片转存中…(img-SS3gcpuS-1663336167114)]