数据结构基础(数组、线性表、栈和队列)

本文详细介绍了数据结构中的基本概念,包括线性表、栈和队列的逻辑结构与存储结构。线性表分为顺序表和链表,栈和队列则探讨了顺序存储和链式存储的特点,以及它们在实际问题中的应用,如括号匹配、表达式求值和层次遍历。此外,还涉及了特殊矩阵的压缩存储策略。
摘要由CSDN通过智能技术生成

基础知识

数据结构基本概念

1)数据

  数据元素:数据基本单位,可由若干数据项组成;

  数据对象:性质相同的数据元素的集合;

  数据类型:包括原子类型,结构类型,抽象数据类型;

  抽象数据类型(ADT):通常用(数据对象,数据关系,基本操作集)这样的三元组表示;

  数据结构:包括逻辑结构、存储结构、数据的运算;

2)数据的逻辑结构(独立于计算机的,与存储结构无关)

  包括:线性结构(线性表,栈,队列),非线性结构(树,图,集合);

3)数据的存储结构(物理结构,不独立于计算机)

  顺序存储:逻辑上相邻的元素存储在物理位置上也相邻的存储单元里;

  链式存储:用指示元素存储地址的指针表示元素之间的逻辑关系;

  索引存储:建立附加索引表,索引项一般为(关键字,地址);

  散列存储:即hash存储,由关键字直接计算出地址;

4)数据的运算:包括运算的定义与实现;

算法和算法评价

1)算法:对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作;

2)五个特性:有穷性,确定性,可行性,输入,输出;

3)时间复杂度:设算法中所有语句的频度之和为T(n),算法中最深层循环内语句的频度为f(n),则T(n)=O(f(n));

  频度:语句在算法中被重复执行的次数;T(n)是问题规模n的函数;

  最好的时间复杂度,平均时间复杂度,最坏时间复杂度; => 一般考虑的是最坏时间复杂度;

  O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)

4)空间复杂度:算法所耗费的存储空间,一般为除输入和程序之外的辅助空间;若算法原地工作,则辅助空间为常量O(1);

5)有序表:关键字有序的线性表,属于逻辑结构;

6)附加说明:

  将两个长度分别为m和n的升序链表,合并为一个长度为m+n的降序链表,最坏情况下的时间复杂度为O(max{m,n}),因为比较次数为2*max{m,n}-1(插空时);

  相同规模n下,复杂度为O(n)的算法在时间上总是优于复杂度为O(2^n)的算法;

  所谓时间复杂度,是指最坏情况下,估算算法执行时间的一个上界;

  同一个算法,实现语言的级别越高,执行效率就越低;

线性表

线性表是一种逻辑结构,顺序表和链表是存储结构

线性表的定义和基本操作

1)线性表:具有相同数据类型的n个数据元素的有限序列;L=(a1,a2,...,ai,ai+1,...,an);

  除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继;

  特征:有限个数据元素;逻辑结构;元素有先后次序(前驱后继关系);数据类型相同;

2)基本操作:

  InitList(&L):初始化

  Length(&L):求表长

  LocateElem(L,e):按值查找

  GetElem(L,e):按位查找

  ListInsert(&L,i,e):插入

  ListDelete(&L,i,&e):删除,用e返回删除元素的值

  PrintList(L):输出

  Empty(L):判空

  DestroyList(&L):销毁

线性表的顺序表示
顺序表的定义

1)顺序表:线性表的顺序存储,特点是表中元素的逻辑顺序与其物理顺序相同

2)线性表的顺序存储类型描述

1.静态分配(数据大小、空间固定)

#define MaxSize 50 //定义线性表的最大长度
typedef struct {
    ElemType data[MaxSize]; //顺序表的元素
    int length; //顺序表的当前长度
}SqList;  //顺序表的类型定义

2.动态分配(动态分配依然是顺序存储结构,随机存取方式,只是分配的空间大小可以在运行时决定)

#define InitSize 100   //表长度的初始定义
typedef struct {
    ElemType *data; //动态分配数组的指针
    int MaxSize,length;  //定义数组的最大容量和当前个数
}SqList;   //动态分配数组顺序表的类型定义C语言

C语言动态分配初始语句 :L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);

C++动态分配初始语句 :L.data=New ElemType(InitSize);

3)顺序表的特点:

  随机存取,随机访问,即首地址+元素序号可以在O(1)时间内找到指定元素;

  存储密度高,每个结点只存储数据元素;

  逻辑上相邻,物理上也相邻,插入和删除需移动大量元素;

顺序表上基本操作的实现(插入,删除,查找)

1)插入操作 

//将元素e插入到顺序表的第i个位置
bool ListInsert(SqList &L,int i, ElemType e) {
    if(i<1 || i>length+1){ //判断i的范围是否有效
       return false;
    } 
    if(L.length > MaxSize){
        return false; //当前存储空间已满,不能插入
    }
    for(int j=L.length;j>=i;j--){ //将第i个元素及之后的元素后移,从后往前后移
        L.data[j]=L.data[j-1];
    }
    L.data[i-1]=e;  //位置i处放入e
    L.length++;  //线性表长度加1
    return true;
}  

最好的情况:在表尾插入(i=n+1),则后移语句不执行,时间复杂度为O(1);

最坏的情况:在表头插入(i=1),则元素后移执行n次,时间复杂度为O(n);

平均情况:在第i个位置插入,元素后移执行n/2次,平均时间复杂度为O(n);

2)删除操作

//删除顺序表中第i个位置的元素
bool ListDelete(SqList &L,int i,ElemType e){
    if(i<1 || i>L.length){ //判断i的范围是否有效
        return false;
    }
    e = L.data[i-1]; //将被删除的元素赋给e
    for(int j=i;j<L.length;j++){ //将第i个元素位置之后的元素前移
        L.data[j-1]=L.data[j];
    }
    L.length--;  //线性表长度减1
    return true;
}

最好情况:删除表尾元素(i=n),无需前移元素,时间复杂度为O(1);

最坏情况:删除表头元素(i=1),前移元素n-1次,时间复杂度为O(n);

平均情况:删除第i个位置的元素,前移(n-1)/2次,平均时间复杂度为O(n);

3)按值查找(顺序查找)

//查找顺序表中值为e的元素,若查找成功,则返回位序,否则返回0;
int LocateElem(SqList L, ElemType e){
    for(int i=0;i<L.length;i++){
        if(L.data[i]==e){
           return i+1; 
    }
    return 0;
}

最好情况:查找元素在表头,仅需比较一次,时间复杂度为O(1);

最坏情况:查找元素在表尾(或不存在)时,需比较n次,时间复杂度为O(n);

平均情况:查找元素在第i个位置,需比较(n+1)/2次,平均时间复杂度为O(n);

 线性表的链式表示

不需使用地址连续的存储单元,即不要求逻辑上相邻的两个元素在物理位置上也相邻,通过“链”建立起数据元素之间的逻辑关系;

单链表

1)定义:线性表的链式存储;每个链表节点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针;

   单链表中结点类型描述:

typedef struct LNode{  //定义单链表结点类型
    ElemType data; //数据域
    struct LNode *next;  //指针域
}LNode,*LinkList;

  优点:解决顺序表需要大量连续存储空间的缺点;

  缺点:由于附加指针域,所以也浪费空间;

  特点:非随机存储结构,查找特定结点时,需要从表头开始遍历,依次查找;

2)通常用"头指针"标识一个单链表L,头指针为"NULL"时表示一个空表;

  头结点:为操作方便,单链表第一个结点之前附加一个结点,称为头结点,头结点的指针域指向第一个结点,数据域可以不设信息,也可记录表长等相关信息;

  头结点&头指针:

  1.不论是否有头结点,头指针始终指向链表的第一个结点;

  2.头结点是带头结点链表的第一个结点,结点内通常不存储任何信息;

  头结点的优点:

  1.链表的第一个位置操作与其他位置一致;

  2.头指针是指向头结点(或链表第一个结点)的非空指针(空表中头结点的指针域为空),所以空表非空表处理一致;

单链表基本操作的实现

1)采用头插法建立单链表

//从空表开始生成新结点
//将新结点插入到当前链表的表头,即头结点之后
LinkList CreateList1(LinkList &L){
    LNode *s;  //新结点
    int x;  //结点值
    L=(LinkList)malloc(sizeof(LNode));  //创建头结点
    L->next = NULL;  //初始为空链表
    scanf("%d",&x);  //输入结点值
    while(x != 9999)   //输入9999表示结束
   {
        s=(LNode*)malloc(sizeof(LNode));  //创建新结点
        s->data = x;
        s->next = L->next;  //将新结点插入表中,L为头指针
        L->next = s;
        scanf("%d",&x);
    }
    return L;
} 

特点: 读入数据的顺序与生成链表中元素顺序相反;设单链表长为n,则时间复杂度为O(n).

2)采用尾插法建立单链表

  将新结点插入到当前链表的表尾上,需要增加一个尾指针r,使其始终指向当前链表的尾结点.(即最后一个结点为r,会更新)

LinkList CreateList2(LinkList &L){
    int x; //元素类型为整型
    L=(LinkList)malloc(sizeof(LNode)); //创建头结点
    LNode *s,*r=L;
    scanf("%d",&x);  //输入结点值
    while(x != 9999)  {  //输入9999表示结束
        s=(LNode*)malloc(sizeof(LNode));  //创建新结点
        s->data = x;
        r->next = s;  //将新结点插入表中,r为表尾指针
        r = s->next;  //r指向新的表尾结点
        scanf("%d",&x);
    }
    r->next = NULL;  //尾结点指针置空
    return L;
}

时间复杂度为O(n),单链表长为n的情况下 

3)按序号查找结点值

//取出单链表L(带头结点)中第i个位置的结点指针(元素从第一个位置开始)
LNode *GetElem(LinkList L,int i){
    int j=1; //计数,初始为1
    LNode *p=L->next;  //将头结点指针赋给p
    if(i==0){
        return L;   //返回头结点
    }
    if(i<0){
        return NULL;  //无效,返回NULL
    }
    while(p && j<i){  //从第1个结点开始查找第i个结点
        p = p->next;
        j++;
    }
    return p;   //返回第i个结点的指针
}

4)按值查找表结点

//查找单链表L(带头结点)中数据域值等于e的结点指针,否则返回NULL
LNode *LocateElem(LinkList L, ElemType e){
    LNode *p=L->next;  //将头结点的指针赋给p
    while(p!=NULL && p->data != e){  //从第1个结点开始查找data域为e的结点
        p = p->next;
    }
    return p;  //指到后返回该结点指针,否则返回NULL
}

5)插入结点操作

//将值为x的新结点插入到单链表的第i个位置上
p = GetElem(L,i-1);  //查找前驱结点
s->next = p->next;
p->next = s;

时间复杂度为O(n)(主要是GetElem(L,i-1)所花费的),若在固定结点后插入,时间复杂度为O(1).

//将*s插入到*p之前
s->next = p->next; //s插入到p之后
p->next = s;
temp=p->data;  //*s,*p交换数据域
p->data = s->data;
s->data = temp;    //时间复杂度为O(1)

6)删除结点操作

//将单链表的第i个结点删除
p=GetElem(L,i-1); //查找删除位置的前驱结点
q=p->next;  //令q指向被删除结点
p->next = q->next; //将*q结点从链中断开
free(q);  //释放结点的存储空间

时间复杂度为O(n)(主要用于GetElem(L,i-1));

//仅知p的后继结点,删除*p
q=p->next;  //删除后继
p->next = q->next; //将后继数据域赋给p
free(q);

7)求表长操作

计算单链表中数据结点(不含头结点)的个数,时间复杂度为O(n)(设置计数器变量),单链表的长度是不包括头结点的;

双链表

1)单链表:只能从前往后顺序遍历,访问前驱的时间复杂度为O(n),访问后继为O(1);

2)双链表:有两个指针prior和next,分别指向其前驱结点和后继结点;

3)双链表中结点类型描述

typedef struct DNode{   //定义双链表结点类型
    ElemType data;  //数据域
    struct DNode *prior,*next;  //前驱和后继指针
}DNode,*DLinkList;

4)双链表的插入操作

//在*p所指结点之后插入结点*s,时间复杂度为O(1)
s->next = p->next;
p->next->prior = s;
p->next = s; //前两行代码必须在该代码之前
s->prior = p;

5)双链表的删除操作

//删除双链表中结点*p的后继结点*q,时间复杂度为O(1)
p->next = q->next;
q->next->prior = p;
free(q);  //释放结点空间
循环链表 

1)循环单链表:与单链表的区别在于,最后一个结点的指针不是NULL,而是指向头结点;

  表尾结点*r的next域指向L,表中没有指针域为NULL的结点;

  判空条件:头结点的指针是否等于头指针;

  因为循环单链表是一个环,所以在任何位置上的插入和删除操作都等价,无需判断表尾;

  单链表:只能从表头结点开始往后顺序遍历整个链表;循环单链表:可以从表中任一结点开始遍历整个链表;

  仅设立尾指针的循环单链表,设r是尾指针,r->next即为头指针,对表头表尾操作都只需O(1)的时间复杂度;

3)循环双链表

  表尾结点的next指向头结点,头结点的prior指向表尾结点;

  循环双链表L中,当结点*p为尾结点时,p->next=L;当为空表时,L->prior=L, L->next=L;

静态链表

1)借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next。此处指针为结点的相对地址(数组下标),静态链表也要预先分配一块连续的存储空间;

 2)静态链表结构类型描述

#define MaxSize 50  //静态链表的最大长度
typedef struct {  //静态链表结构类型的定义
    ElemType data;  //存储数据元素
    int next;      //下一个元素的数组下标
}SLinkList[MaxSize]; 

  静态链表以next=-1作为其结束的标志(设单链表使用方便)

顺序表和链表的比较

1)如何选取存储结构

  存储考虑:当线性表长度或存储规模难以估计 =>链表;

  运算考虑:按序号访问 =>顺序表;插入、删除=>链表;

  环境考虑:顺序表基于数组,链表基于指针;

栈和队列

 栈

基本概念

1)栈:只允许在一端进行插入或删除操作的线性表;

2)栈顶:线性表允许进行插入和删除的那一端;

3)栈底:固定的,不允许进行插入和删除的那一端;

4)空栈:不含任何元素的空表;

5)栈特点:后进先出;

6)基本操作:

  InitStack(&S):初始化一个空栈S;

  StackEmpty(S):判断栈是否为空,空返回true,不空返回false;

  Push(&S,x):进栈;

  Pop(&S,x):出栈

  GetTop(S,&x):读栈顶元素

  ClearStack(&S):销毁栈,并释放栈S占用的存储空间;

栈的顺序存储结构(顺序栈)

1)顺序栈:栈的顺序存储,利用一组地址连续的存储单元存放自栈底至栈顶的数据元素,同时附设一个指针top指示当前栈顶的位置;

2)栈的顺序存储类型描述:

#define MaxSize 50  //定义栈中元素的最大个数
typedef struct{
    ElemType data[MaxSize]; //存放栈中元素
    int top;   //栈顶指针
}SqStack;

初始栈顶指针S.top=-1; 栈顶元素S.data[S.top];栈空:S.top=-1;栈满:S.top=MaxSize-1;栈长S.top+1;进栈+1,出栈-1;

3)顺序栈的基本运算

1.初始化

void InitStack(&S){
    S.top=-1;  //初始化栈顶指针
}

2.判栈空

bool StackEmpty(S){
    if(S.top==-1){  //栈空
        return true;
    }
    else{    //不空
        return false;
}    

3.进栈

bool Push(SqStack &S,ElemType x){
    if(S.top=MaxSize-1){    //栈满,报错
        return false;
    }
    S.data[++S.top]=x;    //进栈,指针加1再进栈
    return true;
}

4.出栈

bool Pop(SqStack &S,ElemType &x){
    if(S.top==-1){    //栈空,报错
        return false;
    }
    x=S.data[S.top--];   //先出栈,指针再减1
    return true;
}

5.读栈顶元素

bool GetTop(SqStack S,ElemType &x){
    if(S.top==-1){   //栈空,报错
        return false;
    }
    x=S.data[S.top];  //x记录栈顶元素
    return true;
}

注意初始条件,S.top=-1表示栈空,S.top=0表示指向第一个元素;

4)共享栈(能有效利用存储空间)

  将栈底设置在共享空间两端,固定不变;栈顶向共享空间的中间延伸;

栈的链式存储结构(链栈)

1)通常用单链表实现,规定所有操作在单链表表头进行,规定链栈没有头结点,Lhead指向栈顶元素;

2)栈的链式存储类型描述

typedef struct Linknode {
    ElemType data;  //数据域
    struct Linknode *next; //指针域
}*LiStack;   //栈类型定义
队列 
队列基本概念

1)队列:简称队,只允许在表的一端进行插入,而在表的另一端进行删除的线性表,先进先出(FIFO);

2)队头:允许删除的一端,删除元素称为出队或离队;

3)队尾:允许插入的一段,插入元素称为入队或进队;

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

5)基本操作:

  InitQueue(&Q):初始化队列;

  QueueEmpty(&Q):判队空;

  EnQueue(&Q,x):入队;

  DeQueue(&Q,&x):出队;

  GetHead(Q,&x):读队头元素,将值赋给x;

队列的顺序存储结构

==>队列的顺序存储

1)队列的顺序存储:分配一块连续的存储单元存放队列中的元素,并设两个指针,front指向当前队头元素的位置,rear指向当前队尾元素的位置。

2)队列顺序存储类型描述:

#define MaxSize 50 //定义队列中元素的最大个数
typedef struct{
    ElemType data[MaxSize];  //存放队列元素
    int front,rear;   //队头指针和队尾指针
}SqQueue;

3)

初始状态 (队空):Q.front = Q.rear = 0;

进队:先送值到队尾,再队尾指针+1;

出队:先取队头值,再队头指针+1;

==>循环队列

1)把存储队列元素的表从逻辑上看作一个环;

  初始:Q.front = Q.rear = 0;

  入队:队尾指针进1取模,Q.rear=(Q.rear+1)%MaxSize;

  出队:队首指针进1取模,Q.front=(Q.front+1)%MaxSize;

  队列长度:(Q.rear+MaxSize-Q.front)%MaxSize;

2)队空队满判定方法

  1.牺牲一个单元,入队时少用一个单元,front在rear下一个位置则队满;

    队空:Q.front == Q.rear;

    队满:Q.front == (Q.rear+1)%MaxSize;

    队列长度:(Q.rear+MaxSize-Q.front)%MaxSize;

  2.增设表示元素个数的数据成员

    队空:Q.size=0;

    队满:Q.size=MaxSize;

  3.增设数据成员tag

    队空:tag=0,删除导致Q.front=Q.rear;

    队满:tag=1,插入导致Q.front=Q.rear;

==>循环队列的操作(采用“牺牲一个单元”判定法则判队空队满)

1)初始化

void InitQueue(&Q){
    Q.front = Q.rear =0;   //初始化队首、队尾指针
}

2)判队空

bool IsEmpty(Q){
    if(Q.rear==Q.front){  //队空条件
        return true;
    }
    else{
        return false;
    }
}

3)入队

bool EnQueue(SqQueue &Q,ElemType x){
    if((Q.rear+1)%MaxSize == Q.front){  //队满
        return false;
    }
    Q.data[Q.rear]=x; //送值至队尾
    Q.rear = (Q.rear+1)%MaxSize; //队尾指针加1取模
    return true;
}

4)出队

bool DeQueue(SqQueue &Q,ElemType &x){
    if(Q.rear == Q.front){  //队空
        return false;
    }
    x=Q.data[Q.front];  //取队头元素值
    Q.front=(Q.front+1)%MaxSize;  //队头指针加1取模
    return true;
}
队列的链式存储结构

==>队列的链式存储

1)队列的链式表示称为链式队列,同时带有队头指针和队尾指针的单链表,头指针指向队头结点,尾指针指向队尾结点;

2)队列链式存储类型描述

typedef struct {  //链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{  //链式队列
    LinkNode *front,*rear; //链式队列队头和队尾指针
}LinkQueue;

  队空:Q.front=NULL and Q.rear=NULL(若是带头结点的链式队列,只需Q.rear=Q.front即可)

  入队:新结点插入链表尾部,即Q.rear指向新结点

  出队:取队头元素,Q.front指向下一结点(若该结点为最后一个结点,则令Q.front,Q.rear均为NULL)

3)链式队列的优点:不存在存储分配不合理和溢出问题,适合数据元素变动较大的问题;

==>链式队列基本操作

1)初始化

void InitQueue(LinkQueue &Q){
    Q.front = Q.rear =(LinkNode *)malloc(sizeof(LinkNode)); // 建立头结点
    Q.front->next = NULL;  //初始为空
}

2)判队空

bool IsEmpty(LinkQueue Q){
    if(Q.rear==Q.front){
        return true;
    }
    else{
        return false;
    }
}

3)入队

void EnQueue(LinkQueue &Q,ElemType x){
    s=(LinkNode *)malloc(sizeof(LinkNode));  //创建新结点
    s->data=x;
    s->next = NULL;
    Q.rear->next = s;   //插入链表尾部
    Q.rear=s;
}

4)出队

bool DeQueue(LinkQueue &Q,ElemType &x){
    if(Q.front==Q.rear){  //队空
        return false;
    }
    p=Q.front->next;
    x=p->data;
    Q.front->next = p->next;
    if(Q.rear ==p){  //若原队列只有一个结点,则删除后变空
        Q.rear = Q.front;
    }
    free(p);
    return true;
}
双端队列

1)允许两端都可以进行入队和出队操作的队列(元素逻辑结构仍是线性结构)

  进队:前端进的在后端进的前面

  出队:先出的元素排在后出元素的前面

2)输出受限的双端队列:允许在一端进行插入和删除,另一端只允许插入的双端队列;

3)输入受限的双端队列:允许在一端进行插入和删除,另一端只允许删除的双端队列;

 不能通过输入受限的双端队列得到的是4,2,3,1和4,2,1,3;

 不能通过输出受限的双端队列得到的是4,2,3,1和4,1,3,2;

栈和队列的应用
栈的应用

1)栈在括号匹配的应用:

  设空栈=>左括号进栈=>右括号,则消除栈中与之最近的左括号(匹配则继续,不匹配则退出);

2)栈在表达式求值中的应用:后缀表达式求值

  操作数进栈=>操作符,从栈中取最上的操作数计算,并将结果压入栈中=>重复上述操作;

3)栈在递归中的应用

  递归=>非递归,需要借助栈;

队列的应用

1)队列在层次遍历中的应用

  根结点入队;队空则结束遍历重复第三步;队列第一个结点出队并访问,若有左孩子,则将左孩子入队,若有右孩子,则将右孩子入队,返回第二步;

2)队列在计算机系统中的应用

  打印机的数据缓冲区(主机和外设速度不匹配的问题);

  CPU资源竞争(队首用户先使用)(多用户引起的资源竞争问题);

特殊矩阵的压缩存储
数组

数组是线性表的推广,一维数组是一个线性表,二维数组可看作线性表的线性表;数组一旦定义,维数和维界不可改变,除初始化和销毁外,只能存取和修改元素;

数组的存储结构

一维数组: A[0,1,...,n-1]

二维数组:按行优先和按列优先

矩阵的压缩存储

压缩矩阵:多个值相同的元素只分配一个存储空间;

1)对称矩阵

 2)三角矩阵

 3)三对角矩阵

 4)稀疏矩阵

-----------------------------------

时间原因,此篇停更,掌握基本概念后,刷题才是硬道理!!!

参考资料:

《王道数据结构》

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值