数据结构笔记①线性表

20 阅读11分钟

一、线性表 定义

元素个数有限,元素为单个数据元素,数据类型相同

二、基本操作

InitList(&L):初始化表。构造一个空的线性表。
Length(L):求表长。返回线性表工的长度,即工中数据元素的个数。
LocateElem(L,e):按值查找操作。在表工中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表工中第i个位置的元素的值。
ListInsert(&L,i,e):插入操作。在表L中的第王个位置上插入指定元素e。
ListDelete(&L,i,&e):删除操作。删除表L中第主个位置的元素,并用e返回删除元素的值。
PrintList(L):输出操作。按前后顺序输出线性表工的所有元素值。
Empty(L):判空操作。若工为空表,则返回true,否则返回false。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表工所占用的内存空间。

三、顺序表:线性表的顺序表示

(一)特点

  1. 元素的逻辑顺序与物理顺序相同
  2. 存储结构:随机存取
  3. 顺序表位序从1开始,数组下标从0开始
  4. 存储密度高,每个结点只存储数据元素
  5. 需要一段连续的存储空间

(二)定义和初始化

1.静态定义:空间满了就溢出
#define Maxsize 50//线性表最大长度 
typedef struct{
    ElemType data[MaxSize];//顺序表的元素
    int length;//顺序表的当前长度
}SqList;//顺序表的类型定义
SqList L;
void InitList(SqList &L) {
    L.length=0;
}

2.动态定义:空间满了可以再开辟一块更大的,将原表元素全部拷贝到新空间
#define InitSize 100 
typedef struct {
    ElemType *data; //指示动态分配数组的指针
    int MaxSize, length; //数组的最大容量和当前个数 
} SeqList;
SeqList L;
void InitList(SeqList &L) {
    L.data=(ElemType*)malloc(InitSize*sizeof(ElemType));//分配存储空间
    L.length=0;
    L.Maxsize=InitSize;//初始存储容量
}

(三)插入

在第i个位置(1<=i<=L.length+1)插入新元素e
时间复杂度 最好在表尾插入,后移不执行 O(1)
最坏在表头插入,后移执行n次 O(n)
平均移动n/2个元素 O(n)

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

(四)删除

删除顺序表L中第i(1<=i<=L.length+1)个位置的元素,用变量e返回
时间复杂度 最好:删除表尾元素(i=n),无需移动, O(1)
最坏:删除表头元素,移动除了表头的所有元素,O(n)
平均O(n)

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; 
}

(五)查找

1.按值查找(顺序查找)

查找顺序表中值为e的元素 时间复杂度 最好:查找表头元素,比较一次, O(1)
最坏:查找表尾元素或不存在,比较n次,O(n)
平均移动(n-1)/2个元素 O(n)

int LocateElem(SqList L,ElemType e){
    int i;
    for(i=0;i < L.length;i++)
    if(L.data[i]==e)
        return i+1; //返回其位序i+1 
    return 0; //退出循环,说明查找失败
}
2.按序号查找

获取表中第i个位置的元素的值
时间复杂度:O(1)

Elemtype GetElem(SqList L,int i){
    return L.data[i-1];
}

四、链表:线性表的链式表示

(一)单链表

1.特点
  1. 非随机存取
2.定义结点
typedef struct LNode{
    ElemType data; //数据域
    struct LNode *next; //指针域 
}LNode,*LinkList;
3.初始化
//带头结点
bool InitList(LinkList &L) {
    L=(LNode*)malloc(sizeof(LNode));//创建头结点
    L->next=NULL;//头结点后暂无元素结点
    return true;
空表判断:L==NULL

//不带头结点
bool InitList(LinkList &L) {
    L = NULL;//头指针初始化为NULL
    return true;
空表判断:L->next==NULL
4.求表长

表长=数据结点的个数
时间复杂度O(n)

//1.带头结点
int Length(LinkList L) {
    int len=0;//计数变量
    LNode *p=L;
    while(p->next != NULL) {
        p = p->next;
        len++;
    }
    return len;
}
//2.不带头结点

5.按序号查找结点

从第一个数据结点开始,找到第i个结点
时间复杂度O(n)

//带头结点
LNode *GetElem(LinkList L,int i){
    int j=1; //计数,初始化为1
    LNode *p=L->next; //头结点指针赋给P 
    if(i==0)
        return L; //若i等于0.则返回头结点 
    if(i<1)
        return NULL; //若i无效,则返回NULL
    while(p&&j<i){ //从第1个结点开始找,查找第i个结点
        p=p->next;
        j++;
    }
    return p; //返回第i个结点的指针,如果i大于表长,p= NULL,直接返回p即可
}
6.按值查找结点

查找数据域值等于e的结点指针
时间复杂度O(n)

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

(1)头插法:将新结点插到头结点之后,从表尾到表头逆向建表。读入数据的顺序与链表元素的顺序相反
时间复杂度O(n)

//带头结点
LinkList List_HeadInsert(LinkList &L){
    LNode *s;int x;
    L=(LinkList)malloc(sizeof(LNode)); //创建头结点 
    L->next=NULL; //初始化为空链表
    while(scanf("%d",&x) != EOF){ //循环输入
        s=(LNode*)malloc(sizeof(LNode));//创建新结点 
        s->data=x;
        s->next=L->next;
        L->next=s; //将新结点插入表中,L为头指针
        scanf("%d",&x); 
    }//while结束
    return L;
}

//不带头结点

应用:链表原地逆置 (2)尾插法:从表头到表尾正向建立单链表L,每次均在表尾插入元素
时间复杂度O(n)

//带头结点
LinkList List_TailInsert(LinkList &L) {
    int x; //设元素类型为整型
    L = (LinkList)malloc(sizeof(LNode)); 
    LNode *s, *r = L; //r为表尾指针
    while(scanf("%d",&x) != EOF) { //循环输入
        s = (LNode*)malloc(sizeof(LNode));
        s->data = x;
        r->next = s;
        r = s; //r指向新的表尾结点 
        scanf("%d",&x);
    }
    r->next = NULL; //尾结点指针置空
    return L;
 }
 
 //不带头结点
8.插入结点

将值为x的结点插到第i个位置

1.按位序列插入

令新结点s指向第i-1个结点p的后继,再令p指向s
时间复杂度O(n)

//1.带头结点
bool ListInsert(LinkList &L,int i,ElemType e){
    LNode *p=L;//指针p指向当前扫描到的结点
    int j=0;//记录当前结点的位序,头结点是第0个结点
    while(p!=NULL&&j<i-1){    //循环找到第i-1个结点
        p=p->next;
        j++;
    }
    if(p==NULL)//i值不合法
        return false;
    LNode *s=(LNode*)malloc(sizeof (LNode));
    s->data=e;
    s->next=p->next;
    p->next=s;
    return true;
}
//p=GetElem(L,i-1); //查找插入位置的前驱结点

//2.不带头结点:需判断插入位置是否为1,若是,将头指针L指向新的首结点
bool ListInsert(LinkList &L,int i, ElemType e){
    if(i<1)
        return false;
    if(i==1){ //插入第1个结点的操作与其他结点操作不同
        LNode *s =(LNode *)malloc(sizeof(LNode));
        s->data=e;
        s->next=L;
        L=S;//头指针指向新结点
        return true;
    }
    LNode *p;//指针p指向当前扫描到的结点
    int j=1;//当前p指向的是第几个结点
    p = L;//p指向第1个结点(注意:不是头结点)
    while(p!=NULL &&j<i-1){ //循环找到第 i-1 个结点
        p=p->next;
        j++;
    }
    if(p==NULL)//i值不合法
        return false;
    LNode *s =(LNode *)malloc(sizeof(LNode));
    s->data =e;
    s->next=p->next;
    p->next=s;
    return true;//插入成功
}
2.后插法:在指定结点后插入结点

在p后插入e
时间复杂度O(1)

bool InsertNextNode(LNode *p,ElemType e){
    if(P==NULL)
        return false;
    LNode_*s_=(LNode _*)malloc(sizeof(LNode));
    s->data =e;
    s->next=p->next;
    p->next=s;
    return true;
}
3.前插法

在p前插入e

//1.循环查找p的前驱q,再对q后插,O(n)
//2.转换成后插,O(1)
下面两个都是
bool InsertPriorNode(LNode *p,LNode*S)
    if(p==NULL||S==NULL)
        return false;
    S->next=p->next;
    p->next=s;//s连到p之后
    ElemType temp=p->data;//交换数据域部分
    p->data=s->data;
    s->data=temp;
    return true;
}

bool InsertPriorNode(LNode *p, ElemType e){
    if(p==NULL)
        return false;
    LNode *s =(LNode *)malloc(sizeof(LNode));
    s->next=p->next;
    p->next=s;//新结点 s连到 p 之后
    s->data=p->data;//将p中元素复制到s中
    p->data=e;//p 中元素覆盖为 e
    return true;
}
9.删除结点

将第i个结点删除
时间复杂度 最坏O(n),最好O(1)(删第一个) 平均O(n)

//带头结点
bool ListDelete(LinkList &L,int i,ElemType &e){
    LNode *p=L;////指针p指向当前扫描到的结点
    int j=0;//记录当前结点的位序,头结点是第0个结点
    while(p->next!=NULL&&j<i-1){//循环找到第i-1个结点
    p=p->next;
    j++;
}
    if(p->next==NULL||j>i-1)//i值不合法
        return false;
    LNode *q=p->next;//令q指向被删除结点
    e=q->data;//用e返回元素的值
    p->next=q->next;//将*q结点从链中“断开”
    free(q);//释放结点的存储空间①
    return true;
    
//不带头结点:需要判断被删结点是否是头结点,若是,要将头指针L指向新的首结点
    

删除所给结点:时间复杂度O(1)
将后继的值赋予自己,再删除后继
若p为最后一个结点,p->next出错,只能从表头开始找,O(n)

bool DeleteNode(LNode *p){
    if(p==NULL)
        return false;
    LNode *q=p->next;//令q指向*p的后继结点
    p->data=p->next->data; //和后继结点交换数据域
    p->next=q->next; //将*q结点从链中“断开”
    free(q); //释放后继结点的存储空间
    return true;
}

(二)双链表

1.结点定义与初始化
typedef struct DNode{ 
    ElemType data; //数据域
    struct DNode *prior,*next; //前驱和后继指针 
}DNode,*DLinkList;

bool InitDLinkList(DLinklist &L){
    L=(DNode *)malloc(sizeof(DNode));//分配一个头结点
    if(L==NULL)//内存不足,分配失败
        return false;
    L->prior = NULL;//头结点的 prior 永远指向 NULL
    L->next = NULL;//头结点之后暂时还没有节点
    return true;
}

判空(带头结点)
bool Empty(DLinklist L){
    if(L->next == NULL)
        return true;
    else
        return false;

2.查找(按值、按位)

与单链表相同

3.插入
(1)后插

p后插入s。时间复杂度O(1)

bool InsertNextDNode(DNode *p,DNode *s){
    if(p==NULL|| S==NULL)//非法参数
        return false;
    s->next=p->next;
    if(p=>next != NULL)//如果p结点有后继结点
        p->next->prior=s;
    s->prior=p;
    p->next=s;
    return true;
(2)前插

找到给定结点的前驱,对其进行后插

(3)按位序插入

找到某位序结点的前驱,对其进行后插

4.删除

删除p的后继结点q

bool DeleteNextDNode(DNode *p){
    if(p==NULL)
        return false;
    DNode *q = p->next;//找到p的后继结点q
    if(G==NULL)
        return false;//p没有后继
    p->next=g->next;
    if(q->next!=NULL)//q结点不是最后一个结点
        q->next->prior=p;
    free(q);//释放结点空间
    return true;
}

删除q的前驱结点p:

5.遍历

时间复杂度O(n)

//后向遍历
while (p!=NULL){
    p= p->next;
}
//前向遍历
while (p!=NULL){
    p= p->prior;
}
//跳过头结点
while (p->prior!=NULL){
    p= p->prior;
}

(三)循环单链表

a3655f60fa395eb181a60a29db9bcea.jpg 若设的是头指针,对在表尾插入元素需要O(n)的时间复杂度,而若设的是尾指针r,r->next即头指针,对在表头或表尾插入元素都只需要O(1)的时间复杂度。

(四)循环双链表

判空:L->next==L
判断p为表尾结点:p->next==L 7cb96375cc4b43b2c3a403d9a2fdd92.jpg

插入:

在P结点之后插入s结点

bool InsertNextDNode(DNode *p,DNode *s){
    s->next=p->next;
    p->next->prior=s;
    s->prior=p;
    p->next=s;
}
删除:

删除p的后继q

p->next=q->next;
q->next->prior=p;
free(q);

(五)静态链表

  1. 预先分配连续内存空间,容量固定不可变,不能随机存取
  2. 数组下标为0是头结点,next==-1为结束标志,初始化a[0]=-1,空结点为-2
  3. 插入、删除与动态链表相同,不用移动元素,只需修改指针
  4. 按位序查找(从头结点出发,遍历):O(n)
  5. 插入位序为i的结点:①找到一个空的结点,存入数据元素A②从头结点出发找到位序为i-1的结点③修改新结点的 next④修改i-1号结点的 next
  6. 删除结点:①从头结点出发找到前驱结点②修改前驱结点的游标③被删除结点 next 设为 -2
#define MaxSize 50 //静态链表的最大长度
typedef struct{ 
    ElemType data; //存储数据元素
    int next; //下一个元素的数组下标
}SLinkList[MaxSize];

(六)对比

1.顺序表
  1. 顺序/随机存取
  2. 逻辑相邻、物理相邻
  3. 对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log₂n)。
  4. 对于按序号查找,顺序表支持随机访问,时间复杂度仅为0(1)
  5. 顺序表的插入、删除操作,平均需要移动半个表长的元素。
  6. 适合查找
1.链表
  1. 顺序存取逻辑相邻、物理相邻
  2. 逻辑相邻不一定物理相邻
  3. 对于按序号查找,链表的平均时间复杂度为O(n)。
  4. 链表的插入、删除操作,只需修改相关结点的指针域即可。
  5. 存储密度低
  6. 适合扩容、增删