数据结构
时间复杂度
数的清为O(1)
线性结构定义:
若结构是非空有限集,则有且仅有一个开始结点和一个终端结点,并且所有结点都最多只有一个直接前趋和一个直接后继。
顺序存储,链式存储
C语言动态分配函数(<stdlib.h>)
malloc(m)开辟m字节长的地址空间
sizeof(x)计算变量的长度
free(p)释放指针p所指变量的存储空间,彻底删除一个变量
C++ new +类型名
int *p=new int;
或int *p =new int(10);
delete p;删除p
线性表
初始化线性表
//初始化线性表
Status InitList_Sq(SqList &L) //引用构造一 个空的顺序表L
{
L.elem=new ElemType[MAXSIZE]; //为顺序表分配空间
if(!L.elem) exit(OVERFLOW); //存储分配失败
L.length=0; //空表长度为0
return OK;
}
Status InitList_Sq(SqList *L) //指针构造一个空的顺序表L
{
L-> elem=new ElemType[MAXSIZE]; //为顺序表分配空间
if(! L-> elem) exit(OVERFLOW); //存储分配失败
L-> length=0; //空表长度为0
return OK;
}
//在线性表L中查找值为e的数据元素
int LocateELem(SqList L, ElemType e)
{
for (i=0;i< L.length;i++)
if (L.elem[i]==e) return i+1;
return 0;
}
//插入
Status ListInsert_Sq(SqList &L, int i , ElemType e)
{
if(i<1 || i>L.length+1) return ERROR; //i值不合法
if(L.length==MAXSIZE) return ERROR; //当前存储空间已满
for(j=L.length-1;j>=i-1;j--)
L.elem[j+1]=L.elem[j]; //插入位置及之后的元素后移
L.elem[i-1]=e; //将新元素e放入第i个位置
++L.length; //表长增1
return OK;
}
//删除
Status ListDelete_Sq(SqList &L, int i)
{
if((i<1)||(i>L.length)) return ERROR; //i值不合法
for (j=i;j<=L.length-1;j++)
L.elem[j-1]=L.elem[j]; //被删除元素之后的元素前移
--L.length; //表长减1
return OK;
}
查找、插入、删除算法的平均时间复杂度为:O(n)
顺序表的空间复杂度S(n)=O(1)
(没有占用辅助空间)
顺序表特点
利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
称为随机存取法
结点由数据域和指针域组成
各结点由两个域组成:
数据域:存储元素数值数据
指针域:存储直接后继结点的存储位置
单链表、双链表、循环链表:
结点只有一个指针域的链表,称为单链表或线性链表
有两个指针域的链表,称为双链表
首尾相接的链表称为循环链表
头指针是指向链表中第一个结点的指针
首元结点是指链表中存储第一个数据元素a1的结点
头结点是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息
分为有头结点和无头结点
空表-头结点指针域为空
设置头结点好处
⒈便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;
⒉便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
数据域
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
链表优缺点
优点
数据元素的个数可以自由扩充
插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
缺点
l存储密度小
l存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)
1. 链表的每个结点中都恰好包含一个指针。
2. 顺序表结构适宜于进行顺序存取,而链表适宜于进行随机存取。
3. 顺序存储方式的优点是存储密度大,且插入、删除运算效率高。
4. 线性表若采用链式存储时,结点之间和结点内部的存储空间都是可以不连续的。
5. 线性表的每个结点只能是一个简单类型,而链表的每个结点可以是一个复杂类型
全错
单链表
typedef struct Lnode
{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList;
// *LinkList为LNode类型的指针
Status InitList_L(LinkList &L){
L=new LNode;
L->next=NULL;
return OK;
}
//销毁
Status DestroyList_L(LinkList &L)
{
LinkList p;
while(L)
{
p=L;
L=L->next;
delete p;
}
return OK;
}
//清空
Status ClearList(LinkList & L){
// 将L重置为空表
LinkList p,q;
p=L->next; //p指向第一个结点
while(p) //没到表尾
{ q=p->next; delete p; p=q; }
L->next=NULL; //头结点指针域为空
return OK;
}
//求表长
int ListLength_L(LinkList L){
//返回L中数据元素个数
LinkList p;
p=L->next; //p指向第一个结点
i=0;
while(p){//遍历单链表,统计结点数
i++;
p=p->next; }
return i;
}
//判断表是否为空
int ListEmpty(LinkList L)
{
//若L为空表,则返回1,否则返回0
if(L->next) //非空
return 0;
else
return 1;
}
//获取线性表L中的某个数据元素的内容
Status GetElem_L(LinkList L,int i,ElemType &e){
p=L->next; j=1; //初始化
while(p&&j<i){ //向后扫描,直到p指向第i个元素或p为空
p=p->next; ++j;
}
if(!p || j>i) return ERROR; //第i个元素不存在
e=p->data; //取第i个元素
return OK;
}//GetElem_L
//在线性表L中查找值为e的数据元素
LNode *LocateELem_L (LinkList L,Elemtype e) {
//返回L中值为e的数据元素的地址,查找失败返回NULL
p=L->next;
while(p &&p->data!=e)
p=p->next;
return p;
}
//在L中第i个元素之前插入数据元素e
Status ListInsert_L(LinkList &L,int i,ElemType e){
p=L;j=0;
while(p&&j<i−1){p=p->next;++j;} //寻找第i−1个结点
if(!p||j>i−1)return ERROR; //i大于表长 + 1或者小于1
s=new LNode; //生成新结点s
s->data=e; //将结点s的数据域置为e
s->next=p->next; //将结点s插入L中
p->next=s; //能和上一条语句互换么?
return OK;
}//ListInsert_L
//将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L,int i,ElemType &e){
p=L;j=0;
while(p->next &&j<i-1){ //寻找第i个结点,并令p指向其前驱
p=p->next; ++j;
}
if(!(p->next)||j>i-1) return ERROR; //删除位置不合理
q=p->next; //临时保存被删结点的地址以备释放
p->next=q->next; //改变删除结点前驱结点的指针域
e=q->data; //保存删除结点的数据域
delete q; //释放删除结点的空间
return OK;
}//ListDelete_L
//前插法
void CreateList_F(LinkList &L, int n){
L=new LNode;
L->next=NULL; //先建立一个带头结点的单链表
for(i=n;i>0;--i){
p=new LNode; //生成新结点
scanf(&p->data); //输入元素值
p->next=L->next; L->next=p; //插入到表头
}
}//CreateList_F
//尾插法
void CreateList_L(LinkList &L,int n){
//正位序输入n个元素的值,建立带表头结点的单链表L
L=new LNode;
L->next=NULL;
r=L; //尾指针r指向头结点
for(i=0;i<n;++i){
p=new LNode; //生成新结点
scanf(&p->data); //输入元素值
p->next=NULL; r->next=p; //插入到表尾
r=p; //r指向新的尾结点
}
}//CreateList_L
//有序表的合并
void MergeList_Sq(SqList LA,SqList LB,SqList &LC){
pa=LA.elem; pb=LB.elem; //指针pa和pb的初值分别指向两个表的第一个元素
LC.length=LA.length+LB.length; //新表长度为待合并两表的长度之和
LC.elem=new ElemType[LC.length]; //为合并后的新表分配一个数组空间
pc=LC.elem; //指针pc指向新表的第一个元素
pa_last=LA.elem+LA.length-1; //指针pa_last指向LA表的最后一个元素
pb_last=LB.elem+LB.length-1; //指针pb_last指向LB表的最后一个元素
while(pa<=pa_last && pb<=pb_last){ //两个表都非空
if(*pa<=*pb) *pc++=*pa++; //依次“摘取”两表中值较小的结点
else *pc++=*pb++; } pa++; //LB表已到达表尾
while(pb<=pb_last) *pc++=*pb++;
while(pa<=pa_last) *pc++=*pa++; //LA表已到达表尾
}//MergeList_Sq
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList
//双向链表插入
Status ListInsert_DuL(DuLinkList &L,int i,ElemType e){
if(!(p=GetElemP_DuL(L,i))) return ERROR;
s=new DuLNode;
s->data=e;
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
return OK;
}
//删除
Status ListDelete_DuL(DuLinkList &L,int i,ElemType &e)
{
if(!(p=GetElemP_DuL(L,i))) return ERROR;
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
delete p;
return OK;
}
循环链表
栈
运算规则:先进后出
![image-20210329212325130](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329212325130.png)
![image-20210329212457307](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329212457307.png)
“进” =压入=PUSH()
“出” =弹出=POP( )
![image-20210329212844014](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329212844014.png)
顺序栈的表示 (top一直指向空)
#define MAXSIZE 100
typedef struct
{
SElemType *base;
SElemType *top;
int stacksize;
}SqStack;
![image-20210329213053838](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329213053838.png)
//顺序栈初始化
Status InitStack( SqStack &S )
{
S.base =new SElemType[MAXSIZE];
if( !S.base ) return OVERFLOW;
S.top = S.base;
S.stackSize = MAXSIZE; //当前已分配的空间
return OK;
}
//判断栈是否为空
bool StackEmpty( SqStack S )
{
if(S.top == S.base)
return true;
else
return false;
}
//求栈长度
int StackLength( SqStack S )
{
return S.top – S.base;
}
//清空栈
Status ClearStack( SqStack S )
{
if( S.base )
S.top = S.base;
return OK;
}
//销毁栈
Status DestroyStack( SqStack &S )
{
if( S.base )
{
delete S.base ;
S.stacksize = 0;
S.base = S.top = NULL;
}
return OK;
}
//进栈
//*
(1)判断是否栈满,若满则出错
(2)元素e压入栈顶
(3)栈顶指针加1
*/
Status Push( SqStack &S, SElemType e)
{
if( S.top - S.base== S.stacksize ) // 栈满
return ERROR;
*S.top++=e;//拆分理解*S.top=e;S.top++;
return OK;
}
//出栈
//*
(1)判断是否栈空,若空则出错
(2)获取栈顶元素e
(3)栈顶指针减1
*/
Status Pop( SqStack &S, SElemType &e)
{
if( S.top == S.base ) // 栈空
return ERROR;
e= *--S.top;//理解--S.top;e=*S.top;(top栈顶为空)
return OK;
}
取顺序栈栈顶元素
//*
判断是否空栈,若空则返回错误
否则通过栈顶指针获取栈顶元素
*//
Status GetTop( SqStack S, SElemType &e)
{
if( S.top == S.base ) return ERROR; // 栈空
e = *( S.top – 1 );// e = *( S.top -- ); ??此处只是取值,top不会减减
return OK;
}
考研试题
![image-20210329214742280](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329214742280.png)
typedef struct{
int top[2], bot[2]; //栈顶和栈底指针
SElemType *V; //栈数组
int m; //栈最大可容纳元素个数
}DblStack;
//初始化一个大小为m的双向栈s
Status Init_Stack(DblStack &s,int m)
{
s.V=new SElemType[m];
s.bot[0]=-1;
s.bot[1]=m;
s.top[0]=-1;
s.top[1]=m;
return OK;
}
//判栈i空否, 空返回1, 否则返回0
int IsEmpty(DblStack s,int i)
{
return s.top[i] == s.bot[i];
}
//判栈满否, 满返回1, 否则返回0
int IsFull(DblStack s)
{
if(s.top[0]+1==s.top[1])
return 1;
else return 0;
}
//栈入
void Dblpush(DblStack &s,SElemType x,int i)
{
if( IsFull (s ) ) exit(1);
// 栈满则停止执行
if ( i == 0 ) s.V[ ++s.top[0] ] = x;
//栈0情形:栈顶指针先加1, 然后按此地址进栈
else s.V[--s.top[1]]=x;
//栈1情形:栈顶指针先减1, 然后按此地址进栈
}
//栈出
int Dblpop(DblStack &s,int i,SElemType &x)
{
if ( IsEmpty ( s,i ) ) return 0;
//判栈空否, 若栈空则函数返回0
if ( i == 0 ) s.top[0]--; //栈0情形:栈顶指针减1
else s.top[1]++; //栈1情形:栈顶指针加1
return 1;
}
链栈表示
运算是受限的单链表,只能在链表头部进行操作,故没有必要附加头结点。栈顶指针就是链表的头指针
//构造链栈
typedef struct StackNode {
SElemType data;
struct StackNode *next;
} StackNode, *LinkStack;
LinkStack S;
//初始化
void InitStack(LinkStack &S )
{
S=NULL;
}
//判断链栈是否为空
Status StackEmpty(LinkStack S)
{if (S==NULL)
return TRUE;
else return FALSE;
}
//链栈进栈
Status Push(LinkStack &S , SElemType e){
p=new StackNode; //生成新结点p
if (!p) exit(OVERFLOW);
//核心
p->data=e; p->next=S; S=p;
return OK;
}
//链栈出栈
Status Pop (LinkStack &S,SElemType &e)
{
if (S==NULL)
return ERROR;
//核心
e = S-> data; p = S; S = S-> next;
delete p;
return OK;
}
//取链栈栈顶元素
SElemType GetTop(LinkStack S)
{
if (S==NULL)
exit(1);
else
return S–>data;
}
栈与递归
//分治法求解递归问题算法的一般形式:
void p (参数表)
{
if (递归结束条件)可直接求解步骤;-----基本项
else p(较小的参数);------归纳项
}
long Fact ( long n )
{
if ( n == 0) return 1;//基本项
else return n * Fact (n-1); //归纳项
}
![image-20210329215755833](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329215755833.png)
![image-20210329215820836](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329215820836.png)
队列
运算规则:先进先出
![image-20210329212559136](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329212559136.png)
![image-20210329220027534](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329220027534.png)
![image-20210329220056662](https://gitee.com/maidang-zc/image/raw/master/img/image-20210329220056662.png)
//一维数组队列
#define M 100 //最大队列长度
typedef struct {
QElemType *base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
}SqQueue;
//循环队列
#define MAXQSIZE 100 //最大长度
typedef struct {
QElemType *base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
}SqQueue;
//循环队列初始化
Status InitQueue (SqQueue &Q){
Q.base =new QElemType[MAXQSIZE]
if(!Q.base) exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
//求循环队列的长度
int QueueLength (SqQueue Q){
return (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
}
//循环队列入队
Status EnQueue(SqQueue &Q,QElemType e){
if((Q.rear+1)%MAXQSIZE==Q.front) return ERROR;
Q.base[Q.rear]=e;
Q.rear=(Q.rear+1)%MAXQSIZE;
return OK;
}
//队头入队
Status FEnQueue(SqQueue &Q,QElemType e){
if(Q.rear==(Q.front-1+MAXQSIZE)%MAXQSIZE) return ERROR;
Q.base[Q.front]=e;
Q.front=(Q.front-1+MAXQSIZE)%MAXQSIZE;
return OK;
}
//循环队列出队
Status DeQueue (LinkQueue &Q,QElemType &e){
if(Q.front==Q.rear) return ERROR;
e=Q.base[Q.front];
Q.front=(Q.front+1)%MAXQSIZE;
return OK;
}
//队尾入队
Status RDeQueue (LinkQueue &Q,QElemType &e){
if(Q.front==Q.rear) return ERROR;
e=Q.base[Q.rear];
Q.rear=(Q.rear-1)%MAXQSIZE;
return OK;
}
链队列-链队列front为空
typedef struct QNode{
QElemType data;
struct Qnode *next;
}Qnode, *QueuePtr;
typedef struct {
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
//链队列初始化
Status InitQueue (LinkQueue &Q){
Q.front=Q.rear=(QueuePtr) malloc(sizeof(QNode));
if(!Q.front) exit(OVERFLOW);
Q.front->next=NULL;
return OK;
}
//destroy
Status DestroyQueue (LinkQueue &Q){
while(Q.front){
Q.rear=Q.front->next;
free(Q.front);
Q.front=Q.rear; }
return OK;
}
//判断链队列是否为空
Status QueueEmpty (LinkQueue Q)
{
return (Q.front==Q.rear);
}
//求链队列的队头元素
Status GetHead (LinkQueue Q, QElemType &e){
if(Q.front==Q.rear) return ERROR;
e=Q.front->next->data;
return OK;
}
//链队列入队
Status EnQueue(LinkQueue &Q,QElemType e){
p=(QueuePtr)malloc(sizeof(QNode));
if(!p) exit(OVERFLOW);
p->data=e; p->next=NULL;
Q.rear->next=p;
Q.rear=p;
return OK;
}
//链队列出队
Status DeQueue (LinkQueue &Q,QElemType &e){
if(Q.front==Q.rear) return ERROR;
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p) Q.rear=Q.front;
delete p;
return OK;
}
串
定义
零个或者多个字符组成的有限序列
把字符串看成一个线性表,定义成结构体形式,可以用链表,也可以是顺序表(结尾用length控制),以下是定义:
//堆分配存储表示,即顺序表,最常用;从第一个开始存字符,第0个不用
typedef struct Str
{
char elem[SIZE];//elem数组用来存放串的元素
int length;//定义一个串长度
}Str;
//块链存储
typedef struct Chunk{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail; //串的头尾指针
int curlen; //串的当前长度
}LString;
1.串的定义:串是由零个或多个组成的有序队列。
2.串的长度:串中字符的数目称为串的长度。
3.空串:由零个字符组成的串叫做空串,空串不包括任何字符,其长度为零。
4.子串:串中任意个连续的字符组成的子序列称为该串的子串,空串是任何串的子串。
5.主串:包含子串的串相应的称为主串。
6.子串的位置,子串第一个字符在主串的位置
串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,两个相等
串的表示:
逻辑关系与线性表相同
串有两种表示形式:顺序存储表示和链式存储表示。
顺序存储表示:串的顺序存储结构简称为顺序串,顺序串中的字符序列被顺序地存放在一组连续的存储单元中,主要有三种实现顺序串的方式。
串的模式匹配算法
确定主串中的所含子串(模式串)第一次出现的位置
1.普通定位匹配法(BF算法)爆破
Index(S,T,pos)
int Index(SString S,SString T,int pos){
int i=pos,j=1;
while(i<=S.length&&j<=T.length){
if(S[i]==T[j]){
++i;++j;//继续比较后继字符
}
else{
i=i-j+2;//回溯 i-j+1回原位置,再+1,到原来位置的下一个
j=1;//指针后退重新开始匹配
}
}
if(j>T.length) return i-T.length;//i=t.length
else
return 0;
}
时间复杂度的分析:我们这里只分析最坏的情况,那就是对于长度为n的模式串和长度为m的主串,模式串前n-1都是同样的字符而且主串的前m-1也是和模式串一样的字符,例如:模式串为:000001,主串为:000000000000000000000001,则对于这种情况的时间复杂度为:其中我们需要回朔:m-n+1次,每次都要比较:n次,所以我们的时间复杂度为:o((m-n+1)*n)即:o(m·n)
KMP算法
字符串模式匹配算法中较为高效的算法之一,其在某次子串匹配母串失败时并未回溯母串的指针而是将子串的指针移动到相应的位置。
get_next()函数
//前缀后缀比较,j=1时next取0,前后缀存在最大k-1个相等时,next为k,其他情况取1.
//next函数的实现,
void get_next(SString T,int &next[])
{
int i=0; //next数组的下标
int j=0; //next值
next[0]=0;
while(i<T.length)
{
if(j==0 || T[i]==T[j]) //如果不存在或这条件符合了,那么就可以得到next值了
{
++i;++j;
next[i]=j;
}
else
j=next[j];
}
}
//next改进nextval
KMP算法实现
//KMP算法的实现
int Index_KMP(SString S,SString T,int pos){
int i=pos,j=1;
while(i<=S.length&&j<=T.length){
if(j==0||S[i]==T[j]){
++i;++j;//继续比较后继字符
}
else{
j=next[j];
}
}
if(j>T.length) return i-T.length;
else
return 0;
}
时间复杂度
O(m+n)
数组
已知字符串S1中存放一段英文,写出算法format(s1,s2,s3,n),将其按给定的长度n格式
void format(char* s1, char *s2,char *s3,int n) {
char* p = s1, * q = s2,*k=s3;
int i = 0;
while (*p != '\0' && *p == ' ')
p++;
if (*p == '\0')
{
printf("s1为空串");
return 0;
}
while (*p != '\0' && i < n)
{
*q = *p;
q++; p++; i++;
}
if (*p == '\0')
{
printf("s1没有n个字符");
*q = '\0';
*k = '\0';
return 0;
}
if (*(--q) == ' ')
{
p--;
while (*p == ' ' && *p != '\0')
p++;
if (*p == '\0')
{
printf("s1串没有n个两端对其的字符串");
return 0;
}
*q = *p;
}
*(++q) = '\0';
while (*p != '\0') { *k = *p; p++; k++; }
*k = '\0';
}
广义表
广义表(Lists,又称列表)是线性表的推广。线性表定义为n>=0个元素a1,a2,a3,…,an的有限序列。线性表的元素仅限于原子项,原子是作为结构上不可分割的成分,它可以是一个数或一个结构,若放松对表元素的这种限制,容许它们具有其自身结构,这样就产生了广义表的概念。
广义表是n (n>=0)个元素a1,a2,a3,…,an的有限序列,其中ai或者是原子项,或者是一个广义表。通常记作LS=(a1,a2,a3,…,an)。LS是广义表的名字,n为它的长度。
通常用圆括号将广义表括起来,用逗号分隔其中的元素。为了区别原子和广义表,书写时用大写字母表示广义表,用小写字母表示原子。若广义表LS(n>=1)非空,则a1是LS的表头,其余元素组成的表(a2,…an)称为LS的表尾。
广义表存储结构
由于广义表(a1,a2,a3,…an)中的数据元素可以具有不同的结构,(或是原子,或是广义表),因此,难以用顺序存储结构表示,通常采用链式存储结构,每个数据元素可用一个结点表示。
由于广义表中有两种数据元素,原子或广义表,因此,需要两种结构的结点:一种是表结点,用以表示列表;一种是原子结点,用以表示原子。
1.仅有表结点有三个域组成:
标志域、指示表头的指针域和指示表尾的指针域
原子结点只需要两个域:标志域和值域
头尾链表存储表示
typedef enum {ATOM,LIST } ElemTag; //ATOM==0:表示原子,LIST==1:表示子表
typedef struct GLNode {
ElemTag tag; //公共部分,用以区分原子部分和表结点
union { //原子部分和表结点的联合部分
AtomType atom; //atom是原子结点的值域,AtomType由用户定义
struct { struct GLNode *hp, *tp;} ptr;
// ptr是表结点的指针域,ptr.hp 和ptr.tp分别指向表头和表尾
};
} *Glist; //广义表类型
这种存储结构的三个特点:
1。除空表的表头指针为空外,对任何非空列表,其表头指针均指向一个表结点,且该结点中的hp域指示列表表头,tp域指向列表表尾(除非表尾为空,则指针为空,否则必为表结点);
2。容易分清列表中原子和子表所在层次。如在列表D中,原子e和a在同一层次上,而b、c和d在同一层次且比e和a低一层,B和C是同一层的子表;
3。最高层的表结点个数即为列表的长度。
2、表结点和原子结点均由三个域组成:标志域、指示表头的指针域和指示表尾的指针域;原子结点的三个域为:标志域、值域和指示表尾的指针域。
扩展线性链表存储表示
typedef enum { ATOM,LIST} ElemTag;
//ATOM==0:表示原子,LIST==1:表示子表
typedef struct GLNode {
ElemTag tag; //公共部分,用以区分原子部分和表结点
union { //原子部分和表结点的联合部分
AtomType atom; //原子结点的值域
struct GLNode *hp; //表结点的表头指针
};
struct GLNode *tp;
//相当于线性链表的next,指向下一个元素结点
} *Glist; //广义表类型Glist 是一种扩展的线性链表
树
树存储结构–双亲
#define MAX_TREE_SIZE 100
//结点结构
typedef struct PTNode {
Elem data;
int parent; // 双亲位置域
} PTNode;
//树结构
typedef struct {
PTNode nodes [MAX_TREE_SIZE];
int r, n; // 根结点的位置和结点个数
} PTree;
//结点同构
孩子表示法
//树结构:
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根结点的位置
} CTree;
//双亲结点结构:
typedef struct {
Elem data;
ChildPtr firstchild; // 孩子链的头指针
} CTBox;
//孩子结点结构:
typedef struct CTNode {
int child;
struct CTNode *next;
} *ChildPtr;
//孩子兄弟表示法
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
树转换为二叉树
(1)加线。在所有兄弟结点之间加一条连线。
(2)去线。树中的每个结点,只保留它与第一个孩子结点的连线,删除它与其它孩子结点之间的连线。
(3)层次调整。以树的根节点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。(注意第一个孩子是结点的左孩子,兄弟转换过来的孩子是结点的右孩子)
森林转换为二叉树
(1)把每棵树转换为二叉树。
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
二叉树转换为树
是树转换为二叉树的逆过程。
(1)加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。
(2)去线。删除原二叉树中所有结点与其右孩子结点的连线。
(3)层次调整。
二叉树转森林
删除右孩子线
假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。
(1)从根节点开始,若右孩子存在,则把与右孩子结点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除…。直到所有这些根节点与右孩子的连线都删除为止。
(2)将每棵分离后的二叉树转换为树。
赫夫曼树–最优二叉树
找结点中最小的两个构成二叉树,再从结点集中取最小两个
typedef struct
{ int weight;
int parent, lch, rch;
}*HuffmanTree
//一个有n个叶子结点的huffman树有2n-1个结点
//huffman code = 2*n-1
void Select(HuffmanTree HT, int len, int& s1, int& s2) {
int x1 = 0, x2 = 0;
int min1 = 1000, min2 = 1000;
for (int i = 1; i <= len; i++) {
if (HT[i].parent == 0 && HT[i].weight < min1) {
min2 = min1;
x2 = x1;
min1 = HT[i].weight;
x1 = i;
}
else if(HT[i].parent == 0 && HT[i].weight < min2){
min2 = HT[i].weight;
x2 = i;
}
}
s1 = x1;
s2 = x2;
}
void CreatHuffmanTree (HuffmanTree HT,int n){
if(n<=1)return;
m=2*n-1;
HT=new HTNode[m+1];//0号单元未用,HT[m]表示根结点
for(i=1;i<=m;++i)
{HT[i].lch=0;HT[i].rch=0;HT[i].parent=0;}
for(i=1;i<=n;++i)
cin>>HT[i].weight;
for( i=n+1;i<=m;++i) //构造 Huffman树
{ Select(HT,i-1, s1, s2);
//在HT[k](1≤k≤i-1)中选择两个其双亲域为0,
// 且权值最小的结点,
// 并返回它们在HT中的序号s1和s2
HT[s1].parent=i; HT[s2].parent=i;
//表示从F中删除s1,s2
HT[i].lch=s1; HT[i].rch=s2 ;
//s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight + HT[s2] .weight;
//i 的权值为左右孩子权值之和
}
}
void CreatHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n){
//从叶子到根逆向求每个字符的赫夫曼编码,存储在编码表HC中
HC=new char *[n+1]; //分配n个字符编码的头指针矢量
cd=new char [n]; //分配临时存放编码的动态数组空间
cd[n-1]=’\0’; //编码结束符
for(i=1; i<=n; ++i){ //逐个字符求赫夫曼编码
start=n-1; c=i; f=HT[i].parent;
while(f!=0){ //从叶子结点开始向上回溯,直到根结点
--start; //回溯一次start向前指一个位置
if (HT[f].lchild= =c) cd[start]=’0’; //结点c是f的左孩子,则生成代码0
else cd[start]=’1’; //结点c是f的右孩子,则生成代码1
c=f; f=HT[f].parent; //继续向上回溯
} //求出第i个字符的编码
HC[i]= new char [n-start]; // 为第i 个字符编码分配空间
strcpy(HC[i], &cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
} // CreatHuffanCode
静态查找表
静态查找表顺序存储的表示
typedef struct
{
ElemType *elem;//数据元素存储空间基址,建表时按实际长度分配,0号单元留空
int length;//表中元素个数
}SSTable;
查找
顺序查找:
int Search_Seq( SSTable ST , KeyType key )
{
//若成功返回其位置信息,否则返回0
ST.R[0].key =key;
for( i=ST.length; ST.R[ i ].key!=key; - - i );
//不用for(i=n; i>0; - -i) 或 for(i=1; i<=n; i++)
return i;
}//时间复杂度一样;查找平均时间几乎减少一半
平均查找时间:n+1/2
顺序查找
算法简单,对表结构无任何要求(顺序和链式)
平均查找长度较大,查找效率较低
n很大时查找效率较低,不宜采用顺序查找
折半查找:顺序表存储,有序排列
int Search_Bin(SSTable ST, KeyType kval)
{//在有序表ST中折半查找其关键字等于 kval 的数据元素,若找到,则函数值为该元素在表中的位置,否则为0
low=1; high=ST.length;//置区间初值
while(low<=high)
{
mid=(low+high)/2;
if(kval == ST.elem[mid].key) return mid;//找到查找元素
else
if(kval < ST.elem[mid].key) high=mid-1;
else low=mid+1;
}
return 0;//顺序表中不存在待查元素
}
折半查找的平均查找长度为
ASL = (n+1)/n * log2 (n+1) - 1;
每次将待查记录所在区间缩小一半,比顺序查找效率高,时间复杂度O(log2 n)
采用顺序存储结构的有序表,不宜用于链式结构
分块查找:
分块查找又称索引顺序查找,其性能介于顺序查找和折半查找之间,它适合对关键字“分块有序”的查找表进行查找操作。
块间有序,块内无序。查找表中的记录按其关键字的大小分成若干块,前一块的最大关键字小于后一块的最大关键字,而各块内部的关键字不一定有序。
查找的过程分为两步进行:先在索引表中进行折半或顺序查找,以确定待查记录“所在块”;然后在已限定的那一块中进行顺序查找。
优点:插入和删除比较容易,无需进行大量移动。
缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算。
适用情况:如果线性表既要快速查找又经常动态变化,则可采用分块查找。
二叉排序树
若其左子树非空,则左子树上所有结点的值均小于根结点的值;
若其右子树非空,则右子树上所有结点的值均大于等于根结点的值;
其左右子树本身又各是一棵二叉排序树
中序后为递增的
查找
若查找的关键字等于根结点,成功
否则
若小于根结点,查其左子树
若大于根结点,查其右子树
在左右子树上的操作类似
递归查找
(1)若二叉排序树为空,则查找失败,返回空指针。
(2)若二叉排序树非空,将给定值key与根结点的关键字T->data.key进行比较:
BSTree SearchBST(BSTree T,KeyType key)
{
if((!T) || key==T->data.key) return T;
else if (key<T->data.key) return SearchBST(T->lchild,key); //在左子树中继续查找
else return SearchBST(T->rchild,key); //在右子树中继续查找
} // SearchBST
删除
删除叶结点,只需将其双亲结点指向它的指针清零,再释放它即可。
被删结点缺右子树,可以拿它的左子女结点顶替它的位置,再释放它。
被删结点缺左子树,可以拿它的右子女结点顶替它的位置,再释放它。
被删结点左、右子树都存在,可以在它的左子树中寻找中序下的最后一个结点(直接前驱),用它的值填补到被删结点中,再来处理这个结点的删除问题。
性能分析
平均查找长度和二叉树的形态有关,即,
最好:log2n(形态匀称,与二分查找的判定树相似)
最坏: (n+1)/2(单支树)
平衡二叉树
- 是「二叉排序树」
- 任何一个节点的左子树或者右子树都是「平衡二叉树」(左右高度差小于等于 1)
2 种「旋转」方式:
左旋
旧根节点为新根节点的左子树
新根节点的左子树(如果存在)为旧根节点的右子树
右旋:
旧根节点为新根节点的右子树
新根节点的右子树(如果存在)为旧根节点的左子树
4 种「旋转」纠正类型:
LL 型:插入左孩子的左子树,右旋
RR 型:插入右孩子的右子树,左旋
LR 型:插入左孩子的右子树,先左旋,再右旋
RL 型:插入右孩子的左子树,先右旋,再左旋
哈希表
课本称为散列查找法,英文名称则为Hash Search
•优点:查找速度极快O(1),查找效率与元素个数n无关
构建考虑因素
除留余数法
Hash(key)=key mod p (p是一个整数)
如何选取p,p<元素个数,p为质数
开放定址法
基本思想:有冲突时就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总能找到,并将数据元素存入。
探测:寻找下一个空位的过程。包含以下三种探测方法。
1线性探测法
将哈希表假象成一个循环表
Hi=(Hash(key)+di) mod m ( 1≤i < m )
其中:m为哈希表长度
di 为增量序列 1,2,…m-1,且di=i
一旦冲突,就找下一个空地址存入
优点:只要哈希表未被填满,保证能找到一个空地址单元存放有冲突的元素。
缺点:可能使第i个哈希地址的同义词存入第i+1个地址,这样本应存入第i+1个哈希地址的元素变成了第i+2个哈希地址的同义词,……,产生“聚集”现象,降低查找效率。
2解决方案二次探测法
Hi=(Hash(key)±di) mod m
其中:m为哈希表长度,m要求是某个4k+3的质数;
di为增量序列 12,-12,22,-22,…,q2
伪随机探测法
Hi=(Hash(key)+di) mod m ( 1≤i < m )
其中:m为哈希表长度
di 为随机数
查找失败的次数就是指:根据哈希函数算出来你所要查找的关键字的位置,如果这个位置存的不是你的目标关键字,那么就按照你所定的存储哈希函数的规则,也就是所在位置+1向后寻找,直到找到你所要的关键字,如果遇到了表中的空位,那么就说明这个表中没有这个关键字,那么查找失败的次数就是你从“通过哈希函数算出的位置”到“表中的第一个遇到的空位”所经过的位数
也就是说,分母指的是哈希表所给定的长度!!!
链地址
基本思想:相同哈希地址的记录链成一单链表,m个哈希地址就设m个单链表,然后用用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构(key%13)
非同义词不会冲突,无“聚集”现象
链表上结点空间动态申请,更适合于表长不确定的情况
效率分析
使用平均查找长度ASL来衡量查找算法,ASL取决于
ü 哈希函数
ü 处理冲突的方法
ü 哈希表的装填因子
排序
插入排序
/* 对长度为n的数组arr执行插入排序 0~n-1为下标
设temp为缓冲域,当插入的数arr[i]小于前一个值,将此数存在temp,i-1处后移至i处*/
void insertionSort(int arr[], int n){
int i,j;
int temp;
for(i=1;i<n;i++)
if (arr[i]<arr[i-1]){
temp=arr[i];
arr[i]=arr[i-1];
for(j=i-1;temp<arr[j];j--)
arr[j+1]=arr[j];
arr[j+1]=temp;
}
}
希尔排序
void ShellInsert(SqList L,int dk){
for(int i=dk+1;i<=L.Length;i++){//elem[0]是哨兵
if(L.elem[i-dk]>L.elem[i]){
L.elem[0]=L.elem[i-dk];
L.elem[i-dk]=L.elem[i];
L.elem[i]=L.elem[0];
i=dk+1;
}
}
}
void ShellInsert(SqList L,int dk){
int i,j;
for(i=dk+1;i<=L.Length;i++)
if(L.elem[i]<L.elem[i-dk]){
L.elem[0]=L.elem[i];
for(j=i-dk;j>0&&(L.elem[0]<L.elem[j]);j-=dk)
L.elem[j+dk]=L.elem[j];
L.elem[j+dk]=L.elem[0];
}
}
void ShellSort(SqList L)
{
/*按增量序列dlta[0…t-1]对顺序表L作Shell排序,假设规定增量序列为5,3,1*/
int k;
int dlta[3]={5,3,1};
int t=3;
for(k=0;k<t;++k)
ShellInsert(L,dlta[k]);
}
冒泡排序
/* 对长度为n的数组arr执行冒泡排序每一位与之后的所有位做比较,若大则交换,在继续比较 */
void bubbleSort(int arr[], int n){
int i,j,temp;
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++){
if(arr[i]>arr[j]){
temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
}
快速排序
void Qsort ( SqList L,int low, int high )
{ int pivotloc;
if(low<high)
{
pivotloc = Partition(L, low, high ) ;
Qsort (L, low, pivotloc-1) ;
Qsort (L, pivotloc+1, high );
}
}
int Partition ( SqList L,int low, int high ){
int temp=L.elem[low];
while(low<high)
{
while(low<high&&L.elem[high]>=temp){
high--;
}
if(low<high){
L.elem[low]=L.elem[high];
low++;
}
while(low<high&&L.elem[low]<temp){
low++;
}
if(low<high){
L.elem[high]=L.elem[low];
high--;
}
}L.elem[low]=temp;
return low;
}
简单选择排序
//找到i+1之后最小的与i处交换
void SelectSort(SqList L){
int i,j;
int min,temp;
for(i=1;i<=L.Length;i++){
min=i;
for(j=i+1;j<=L.Length;j++)
if(L.elem[j]<L.elem[min]) min=j;
if(i!=min){
temp=L.elem[i];
L.elem[i]=L.elem[min];
L.elem[min]=temp;
}
}
}
堆排序
#include <stdio.h>
#include <malloc.h>
void HeapAdjust(int a[],int s,int m)//一次筛选的过程
{
int rc,j;
rc=a[s];
for(j=2*s;j<=m;j=j*2)//通过循环沿较大的孩子结点向下筛选
{
if(j<m&&a[j]<a[j+1]) j++;//j为较大的记录的下标
if(rc>a[j]) break;
a[s]=a[j];s=j;
}
a[s]=rc;//插入
}
void HeapSort(int a[],int n)
{
int temp,i,j;
for(i=n/2;i>0;i--)//通过循环初始化顶堆
{
HeapAdjust(a,i,n);
}
for(i=n;i>0;i--)
{
temp=a[1];
a[1]=a[i];
a[i]=temp;//将堆顶记录与未排序的最后一个记录交换
HeapAdjust(a,1,i-1);//重新调整为顶堆
}
}
int main()
{
int n,i;
scanf("%d",&n);
int a[n+1];
for(i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
HeapSort(a,n);
}
归并排序
void MergeSort(SqList L,int low,int high)
{
/*用分治法进行二路归并排序*/
int mid;
if(low<high) /*区间长度大于1*/
{
mid=(low+high)/2; /*分解*/
MergeSort(L,low,mid); /*递归地对low到mid序列排序 */
MergeSort(L,mid+1,high); /*递归地对mid+1到high序列排序 */
Merge(L,low,mid,high); /*归并*/
}
}
void Merge(SqList L,int low,int m,int high){
int i=low;
int j=m+1;
int t=1;
int nums[L.Length+1];
while((i<=m)&&(j<=high)){
if(L.elem[i]<L.elem[j])
nums[t++]=L.elem[i++];
else
nums[t++]=L.elem[j++];
}
while (i <= m)
{
nums[t++] = L.elem[i++];
}
while (j <= high)
{
nums[t++] = L.elem[j++];
}
t = 1;
while (low <= high)
{
L.elem[low++] = nums[t++];
}
}
1、冒泡排序不管序列是怎样,都是要比较n(n-1)/2 次的,最好、最坏、平均时间复杂度都为O(n²),需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。
2、选择排序是冒泡排序的改进,同样选择排序无论序列是怎样的都是要比较n(n-1)/2次的,最好、最坏、平均时间复杂度也都为O(n²),需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。
3、插入排序不同,如果序列是完全有序的,插入排序只要比较n次,无需移动时间复杂度为O(n),如果序列是逆序的,插入排序要比较O(n²)和移动O(n²) ,所以平均复杂度为O(n²),最好为O(n),最坏为O(n²),排序过程中只要一个辅助空间,所以空间复杂度O(1)。
4、快速排序的时间复杂度最好是O(nlogn),平均也是O(nlogn),最坏情况是序列本来就是有序的,此时时间复杂度为O(n²),快速排序的空间复杂度可以理解为递归的深度,而递归的实现依靠栈,平均需要递归logn次,所以平均空间复杂度为O(logn)。
5、归并排序需要一个临时temp[]来储存归并的结果,空间复杂度为O(n),时间复杂度为O(nlogn),可以将空间复杂度由 O(n) 降低至 O(1),然而相对的时间复杂度则由 O(nlogn) 升至 O(n²)。
6、希尔排序的时间复杂度分析及其复杂,有的增量序列的复杂度至今还没人能够证明出来,只需要记住结论就行,{1,2,4,8,…}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n²),Hibbard提出了另一个增量序列{1,3,7,…,2k-1},这种序列的时间复杂度(最坏情形)为O(n1.5),Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,…},需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。
7、堆排序的时间复杂度,主要在初始化堆过程和每次选取最大数后重新建堆的过程,初始化建堆时的时间复杂度为O(n),更改堆元素后重建堆的时间复杂度为O(nlogn),所以堆排序的平均、最好、最坏时间复杂度都为O(nlogn),堆排序是就地排序,空间复杂度为常数O(1)。
8、基数排序对于 n 个记录,执行一次分配和收集的时间为O(n+r),如果关键字有 d 位,则要执行 d 遍,所以总的时间复杂度为 O(d(n+r))。该算法的空间复杂度就是在分配元素时,使用的桶空间,空间复杂度为O(r+n)=O(n)
排序算法适用场景
-
- 若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好,否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
- 若序列初始状态基本有序,则直接插入和冒泡最佳,随机的快速排序也不错。插入排序对部分有序的数组很有效,所需的比较次数平均只有选择排序的一半。
- 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
- 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
- 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。但这两种排序都是不稳定的。
- 若要求排序稳定,则可选用归并排序。两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
- 希尔排序比插入排序和选择排序要快得多,并且数组越大,优势越大。如果需要解决一个排序问题而又没有系统排序函数可用(例如直接接触硬件或者运行于嵌入式系统中的代码),可以先用希尔排序,再考虑是否替换为更复杂的排序算法。而对于部分有序和小规模的数组,应使用插入排序。
- 归并排序可以处理数百万甚至更大规模的数组,但是插入排序和选择排序做不到。归并排序的主要缺点是辅助数组所使用的额外空间和n的大小成正比。
- 快速排序的优点是原地排序(只需要一个很小的辅助栈),但是基准的选取是个问题,对于小数组,快速排序要比插入排序慢。
- 堆排序的优点是在排序时可以将需要排序的数组本身作为堆,无需任何额外空间,与选择排序有些类似,但所需的比较要少得多,堆排序适合例如嵌入式系统或低成本移动设备中容量有限的场景。
复杂度为O(n²),快速排序的空间复杂度可以理解为递归的深度,而递归的实现依靠栈,平均需要递归logn次,所以平均空间复杂度为O(logn)。
5、归并排序需要一个临时temp[]来储存归并的结果,空间复杂度为O(n),时间复杂度为O(nlogn),可以将空间复杂度由 O(n) 降低至 O(1),然而相对的时间复杂度则由 O(nlogn) 升至 O(n²)。
6、希尔排序的时间复杂度分析及其复杂,有的增量序列的复杂度至今还没人能够证明出来,只需要记住结论就行,{1,2,4,8,…}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n²),Hibbard提出了另一个增量序列{1,3,7,…,2k-1},这种序列的时间复杂度(最坏情形)为O(n1.5),Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,…},需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。
7、堆排序的时间复杂度,主要在初始化堆过程和每次选取最大数后重新建堆的过程,初始化建堆时的时间复杂度为O(n),更改堆元素后重建堆的时间复杂度为O(nlogn),所以堆排序的平均、最好、最坏时间复杂度都为O(nlogn),堆排序是就地排序,空间复杂度为常数O(1)。
8、基数排序对于 n 个记录,执行一次分配和收集的时间为O(n+r),如果关键字有 d 位,则要执行 d 遍,所以总的时间复杂度为 O(d(n+r))。该算法的空间复杂度就是在分配元素时,使用的桶空间,空间复杂度为O(r+n)=O(n)
排序算法适用场景
-
- 若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好,否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
- 若序列初始状态基本有序,则直接插入和冒泡最佳,随机的快速排序也不错。插入排序对部分有序的数组很有效,所需的比较次数平均只有选择排序的一半。
- 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
- 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
- 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。但这两种排序都是不稳定的。
- 若要求排序稳定,则可选用归并排序。两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
- 希尔排序比插入排序和选择排序要快得多,并且数组越大,优势越大。如果需要解决一个排序问题而又没有系统排序函数可用(例如直接接触硬件或者运行于嵌入式系统中的代码),可以先用希尔排序,再考虑是否替换为更复杂的排序算法。而对于部分有序和小规模的数组,应使用插入排序。
- 归并排序可以处理数百万甚至更大规模的数组,但是插入排序和选择排序做不到。归并排序的主要缺点是辅助数组所使用的额外空间和n的大小成正比。
- 快速排序的优点是原地排序(只需要一个很小的辅助栈),但是基准的选取是个问题,对于小数组,快速排序要比插入排序慢。
- 堆排序的优点是在排序时可以将需要排序的数组本身作为堆,无需任何额外空间,与选择排序有些类似,但所需的比较要少得多,堆排序适合例如嵌入式系统或低成本移动设备中容量有限的场景。