笔记:数据结构——第二章 线性表

第二章 线性表

2.1 线性表的定义和基本操作

  • 线性表(Linear List)是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表;若用L命名线性表,则其一般表示为:L=(a1,a2,…,an)
  • ai是线性表中的“第i个”元素线性表中的位序;a1是表头元素,an是表尾元素;除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继
  • 注意:位序从1开始,数组下标从0开始
  • C语言函数的定义:<返回值类型>函数名(<参数1类型>参数1,<参数2类型>参数2,…)
  • 函数名、参数的形式和命名都可以改变,但要有可读性
  • 传入引用“&”,对参数的修改结果需要带回来

2.2 顺序表

2.2.1 顺序表的定义

  • 顺序表:用顺序存储的方式实现线性表顺序存储
  • 把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的领接关系来体现
  • C语言中知道一个数据元素的大小:sizeof(ElemType)
  • 顺序表中也可以存放结构类型的数据,命名为Customer的结构,其中包含两个整数,即此数据类型占8个字节
typedef sruct{
    int num;
    int people;
}Customer
*顺序表的实现——静态分配
#define MaxSize 10					//定义最大长度
typedef struct{
	ElemType data[MaxSize];			//用静态的"数组"存放数据元素
	int length; 					//顺序表的当前长度  
}SqList;       						//顺序表的类型定义(静态分配方式)
  • 给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)
  • Sq:sequence ——顺序,序列
#include<stdio.h>
#define MaxSize 10   
typedef struct{
	int data[MaxSize];
	int length;
}SqList;       

//基本操作——初始化一个顺序表
void InitList(SqList &L){
	 for(int i=0;i<MaxSize;i++)
	 	L.data[i]=0;  				//将所有数据元素设置为默认初始值
	 L.length=0;
}

int main(){
	SqList L;						//声明一个顺序表
	InitList(L);					//初始化一个顺序表
    //尝试“违规”打印整个data数组
	for(int i=0;i<MaxSize;i++){
		printf("data[%d]=%d\n",i,L.data[i]);
	}
	return 0; 
}
  • 如果没有设置数据元素的默认值,内存中会有遗留的“脏数据”
  • 在内存中分配存储顺序表L的空间,包括MaxSize*sizeof(ElemType)和存储length的空间
*顺序表的实现——动态分配
#define InitSize 10 				//默认的最大长度
typedef struct{
	ElemType  *data;				//指示动态分配数组的指针
	int MaxSize;					//顺序表的最大容量
	int length;  					//顺序表的当前长度 
}SeqList; 							//顺序表的类型定义(静态分配方式)
  • C++动态申请和释放内存空间:new、delete关键字
  • C语言动态申请和释放内存空间:malloc、free函数
  • L.data =(ElemType*)malloc(InitSize*sizeof(ElemType)) ;malloc函数返回一个指针,需要强制转型为定义的数据元素类型指针(ElemType*)
#include<stdlib.h>//malloc、free函数的头文件 
#define InitSize 10 
typedef struct{
	int  *data;
	int MaxSize; 
	int length;  
}SeqList; 

void InitList(SeqList &L){
	L.data =(int*)malloc(InitSize*sizeof(int));	//用malloc 函数申请一片连续的存储空间
	L.length=0;
	L.MaxSize=InitSize;
} 

//增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
	int *p=L.data;
	L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));
	for(int i=0;i<L.length;i++){
		L.data[i]=p[i];   			//将数据复制到新区域 
	}
	L.MaxSize=L.MaxSize+len; 		//顺序表最大长度增加len
	free(p);  						//释放原来的内存空间 
} 

int main(void){
	SeqList L; 
	InitList(L);
    //......
	IncreaseSize(L,5);
	return 0; 
}
  • 虽然动态分配可以改变顺序表的大小,但时间开销大
  • realloc函数也可实现增加数组长度
  • 顺序表的特点:
    1. 随机访问 ,即可以在O(1)时间内找到第i个元素
    2. 存储密度高,每个节点只存储数据元素
    3. 拓展容量不方便
    4. 插入、删除操作不方便,需要移动大量元素

2.2.2(1) 顺序表的插入删除

*顺序表的基本操作——插入

插入操作:在表L中的第i个位置上插入指定元素e

#define MaxSize 10    
typedef struct{
    int data[MaxSize];  
    int Length;        
}SqList; 

//基本操作——在L的位序i处插入元素e
void ListInsert(SqList &L, int i, int e){ 
    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;
}

int main(){
    SqList L;          
    InitList(L);      
    //...插入几个元素
    ListInsert(L,3,3);
    return 0;
}

//bool类型实现插入操作——具有健壮性
bool ListInsert(SqList &L, int i, int 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--){  
        L.data[j]=L.data[j-1];
    }
    L.data[i-1]=e; 
    L.length++;     
    return true;
}

  • 注意:位序、数组下标的关系,并从后面的元素依次移动

  • 平均时间复杂度 : T(n)=O(n)

*顺序表的基本操作——删除

删除操作:删除表L中第i个位置的元素,并用e返回删除元素的值

bool LisDelete(SqList &L, int i, int &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;
}

int main(){
    SqList L;          				
    InitList(L);    
    //...插入几个元素
    int e = -1;        				//用变量e把删除的元素“带回来”
    if(LisDelete(L,3,e))
        printf("已删除第三个元素,删除元素值=%d\n",e);
    else
        printf("位序i不合法,删除失败\n");
    return 0;
}
  • 注意:位序、数组下标的关系,并从前面的元素依次移动

  • 平均时间复杂度 : T(n)=O(n)

2.2.2(2) 顺序表的查找

*顺序表的按位查找
  • 按位查找操作:获取表L中第i个位置的元素的值
//基于静态分配的代码实现
#define MaxSize 10            
typedef struct{
    ElemType data[MaxSize];  
    int Length;            
}SqList;                    

ElemType GetElem(SqList L, int i){
    return L.data[i-1];      		//注意是i-1
}

//基于动态分配的代码实现
#define InitSize 10 
typedef struct{
    ElemType *data;  
    int MaxSize;  
    int length;      
}SeqList;

ElemType GetElem(SqList L, int i){
    return L.data[i-1]; 			//指针也能用数组下标,和访问普通数组的方法一样
}
  • 平均时间复杂度 : T(n)=O(1);具有“随机存取”特性
*顺序表的按值查找
  • 按值查找操作:在表L中查找具有给定关键字值的元素
#define InitSize 10            
typedef struct{
    ElemTyp *data; 
    int Length;              
}SqList;   

int LocateElem(SqList L, ElemType e){
    for(int i=0; i<L.lengthl i++)
        if(L.data[i] == e)  
            return i+1;     		//数组下标为i的元素值等于e,返回其位序i+1
    return 0;               		//退出循环,说明查找失败
}
  • 基本数据类型:int、char、double、float等可以直接用运算符“==”比较;但如果为结构类型不能直接进行比较,需要依次对比各个分量来判断两个结构体是否相等,更好的办法为定义一个函数比较
typedef struct{
    int num;
    int people;
}Customer;

void test(){
    Customer a;
    a.num=1;a.people=1;
    Customer b;
    b.num=1;b.people=1;
    if (a.num == b.num && a.people == b.people){
        printf("相等");
    }else{
        printf("不相等");
    }
}

//更好的办法:定义一个函数
bool isCustomerEqual(Customer a,Customer b){
    if(a.num == b.num && a.people == b.people)
        return true;
    else
        return false;
}
  • 平均时间复杂度 : T(n)=O(n)

2.3 单链表

2.3.1 单链表的定义

  • 单链表:每个结点除了存放数据元素外,还要存储指向下一个结点的指针

  • 顺序表的优点:可随机存取,存储密度高;缺点:要求大片连续空间,改变容量不方便

  • 链表的优点:不要求大片连续空间,改变容量方便;缺点不可随机存取,要耗费一定的空间存放指针

  • typedef关键字——数据类型重命名;使用方法:typedef <数据类型> <别名> 例如:typedef int zhengshu;

*单链表的定义
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}Lnode,*Linklist;	

//相当于上述代码
struct LNode{					
	ElemType data;
	struct LNode *next;
}
typedef struct LNode LNode;			//强调这是一个单链表
typedef struct LNode *Linklist;		//强调这是一个结点
*不带头节点的单链表初始化
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}Lnode,*Linklist;	

//初始化一个空的单链表
bool InitList(LinkList &L){
    L=NULL;							//没有结点设为空表,为防止脏数据
    return true;
}

void test(){
    Linklist L;
    InitList (L);
    //...后续代码...
}

//判断单链表是否为空
bool Empty(LinkList L){
    if(L==NULL)
        return true;
    else
        return false;
}	
//或
bool Empty(LinkList L){
    return(L==NULL);     
}
*带头节点的单链表初始化
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}Lnode,*Linklist;	

//初始化一个空的单链表
bool InitList(LinkList &L){
    L=(LNode*)malloc(sizeof(LNode));
    if(L==NULL)					//内存不足,分配失败
        return false;
    L->next=NULL;
    return true;
}

void test(){
    Linklist L;
    InitList (L);
    //...后续代码...
}

//判断单链表是否为空
bool Empty(LinkList L){
    if(L->next==NULL)
        return true;
    else
        return false;
}	
  • 带头结点的代码,操作更方便

2.3.2(1) 单链表的插入删除

*按位序插入(带头结点)
  • 插入操作:在表L中的第i个位置上插入指定元素e
  • 操作分析:
    1. 找到第i-1个结点
    2. 将新结点插入其后
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}Lnode,*Linklist;	

bool ListInsert(LinkList &L,int i,ElemType e){
    if(i<1)
        return false;
    LNode *p;
    int j=0;					//j表示当前p指向的是第几个结点
    p=L;						//p指向头结点L,头结点是第0个结点
    while(p!=NULL&&j<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;					//将结点s连到p之后
    return true;
}
  • 平均时间复杂度 : T(n)=O(n)
*按位序插入(不带头结点)
bool ListInsert(LinkList &L,int i,ElemType e){
    if(i<1)
        return false;
    if(i==1){					//插入第一个结点与其他结点操作不同
        LNode *s=(LNode*)malloc(sizeof(LNode));	
        s->data=e;
        s->next=L;
        L=s;
        return true;
    }
    LNode *p;
    int j=1;
    p=L;						//p指向第一个结点(不是头结点)
    while(p!=NULL&&j<i-1){		//后续逻辑和带头结点的相同
        p=p->next;
        j++;			
    }
    if(p==NULL)
        return false;		
    LNode *s=(LNode*)malloc(sizeof(LNode));		
    s->data=e;
    s->next=p->next;
    p->next=s;
    return true;
}
*指定结点的后插操作
  • 后插操作:在p结点之后插入元素e
bool InsertNextNode(LNode *p,ElemType e){
    if(p==NULL)
        return false;
     LNode *s=(LNode*)malloc(sizeof(LNode));	
    if(s==NULL)					//内存分配失败,如内存不足
        return false;
    s->data=e;					//用结点s保存元素e
    s->next=p->next;
    p->next=s;
    return true;
}

//“封装” 在第i个位置插入元素e也可以简化为
bool ListInsert(LinkList &L,int i,ElemType e){
    if(i<1)
        return false;
    LNode *p;
    int j=0;					//当前p指向的是第几个结点
    p=L;						//p指向头结点
    while(p!=NULL&&j<i-1){	
        p=p->next;
        j++;			
    }
    return InsertNextNode(p,e);	//进行后插操作
}
*指定结点的前插操作
  • 前插操作:在p结点之前插入元素e或结点s

  • 方法一:传入头结点,循环查找p的前驱q,再对q后插,时间复杂度为O(n)

  • 方法二:申请新结点,作为p的后继结点,交换两结点数据,时间复杂度为O(1)

//在p结点之前插入元素e
bool InsertPriorNode(LNode *p,ElemType e){
    if(p==NULL)
        return false;
     LNode *s=(LNode*)malloc(sizeof(LNode));	
    if(s==NULL)					
        return false;
    s->next=p->next;
    p->next=s;
    s->data=p->data;
    p->data=e;
    return true;
}

//在p结点之前插入结点s
bool InsertPriorNode(LNode *p,LNode *s){
    if(p==NULL||s==NULL)					
        return false;
    s->next=p->next;
    p->next=s;
    ElemType temp=p->data;		//交换数据域部分
    p->data=s->data;
    s->data=temp;
    return true;
}
*按位序删除(带头结点)
  • 删除操作:删除表L中第i个位置的元素,并用e返回删除元素的值
  • 操作分析:
    1. 找到第i-1个结点,指向第i+1个结点
    2. 释放第i个结点
bool ListDelete(LinkList &L,int i,ElemType &e){
    if(i<1)
        return false;
    LNode *p;
    int j=0;					
    p=L;						
    while(p!=NULL&&j<i-1){	
        p=p->next;
        j++;			
    }
    if(p==NULL)
        return false;			//与按位序插入相似
    if(p->next=NULL)			//第i-1个结点之后无其他结点
        return false;
    LNode *q=p->next;			//q指向被删结点
    e=q->data;					//e返回被删元素
    p->next=q->next;			//断开,指向第i+1个结点
    free(q);
    return true;
}
  • 平均时间复杂度 : T(n)=O(n)
*指定结点的删除
  • 方法一:传入头结点,循环查找p的前驱q,再删除

  • 方法二:q指针指向p的后继结点,后q数据复制到前p,指向q的下一结点并删除q

bool DeleteNode(LNode *p0){
    if(p==NULL)
        return false;
    LNode *q=p->next;
    p->data=p->next->data;		//存在bug:可能p指针为最后一个结点
    p->next=q->next;			//此时,只能用方法一
    free(q):
    return ture;
}
  • 如果p是最后一个结点,只能从表头开始依次寻找p的前驱
  • Tips:体会有“头结点”和“封装”的代码

2.3.2(2) 单链表的查找

*按位查找
  • 按位查找:获取表L中第i个位置元素的值,返回第i个元素(与上述查找第i-1个类似)
  • 平均时间复杂度 : T(n)=O(n)
LNode *GetELem(LinkList L,int i){
    if(i<0)						//考虑i=0时,为头结点情况;p=L指向头结点
        return false;
    LNode *p;
    int j=0;				
    p=L;						
    while(p!=NULL&&j<i){		//i值不合法(大于链长);循环找到第i个结点
        p=p->next;
        j++;			
    }
    return p;
}

//第二种写法
LNode *GetELem(LinkList L,int i){
    int j=1;					//p指向首元结点
    LNode *p=L->next;
    if(i==0)					//考虑i=0时,为头结点情况;
        return L;
    if(i<1)
        return NULL;					
    while(p!=NULL&&j<i){	
        p=p->next;
        j++;			
    }
    return p;
}

//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L,int i,ElemType e){
    if(i<1)
        return false;
   	
    LNode *p=*GetELem(L,int i-1);			
    
    return InsertNextNode(p,e);
}

//补充上述return后的函数
bool InsertNextNode(LNode *p,ElemType e){
    if(p==NULL)					//有必要,具有健壮性
        return false;			//考虑到GetELem函数的非法情况会返回NULL
     LNode *s=(LNode*)malloc(sizeof(LNode));	
    if(s==NULL)					
        return false;
    s->data=e;					
    s->next=p->next;
    p->next=s;
    return true;
}


//删除第i个位置的元素
bool ListDelete(LinkList &L,int i,ElemType &e){
    if(i<1)
        return false;
   
    LNode *p=*GetELem(L,int i-1);
    
    if(p==NULL)					
        return false;			
    if(p->next=NULL)			
        return false;
    LNode *q=p->next;			
    e=q->data;				
    p->next=q->next;			
    free(q);
    return ture;
}
  • 体会“封装”的好处:避免重复代码,简介、易维护
*按值查找
  • 按值查找:在表L中查找给定关键字值e的元素
LNode *LocateElem(LinkList L,ElemType e){
    LNode *p=L->next;
    while(p!=NULL&&p->data!=e)
        p=p->next;
    return p;
}
  • 如果数据类型为结构类型就不能使用!=符号
*求表的长度
int Length(LinkList L){
    int len=0;
    LNode *p=L;
    while(p->next!=NULL){
        p=p->next;
        len++;
    }
    return len;
}
  • 平均时间复杂度 : T(n)=O(n)

2.3.2(3) 单链表的建立

*尾插法
  • 操作分析:

    1. 初始化单链表 InitList(L)

    2. 设置变量length记录链表长度

    3. while{
          //每次取一个元素e;
          //调用ListInsert(L,length+1,e);把元素插到尾部
          //length++;
      }
      
  • ListInsert(L,length+1,e)每次找表尾都要循环一次,时间复杂度O(n);可以设置一个表尾指针,然后进行后插操作

LinkList List_TailInsert(LinkList &L){		//尾插法 正向建立单链表
    int x;									//设ElemType为整形
    L=(LinkList)malloc(sizeof(LNode));		//建立头结点,初始化空表
    LNode *s,*r=L;							//s为新结点,r为表尾指针,初始指向头结点
    scanf("%d",&x);
    while(x!=9999){							//9999任意设置结束条件
        s=(LNode*)malloc(sizeof(LNode));
        s->data=x;
        r->next=s;
        r=s;								//表尾指针r指向新的表尾结点
        scanf("%d",&x);
    }
    r->next=NULL;							//尾结点指针置空
    return L;
}
*头插法
  • 操作分析:

    1. 初始化单链表 InitList(L)

    2. while{
          //每次取一个元素e;
          //调用InsertNextNode(L,e);在头结点进行后插操作;
          //length++;
      }
      
LinkList List_HeadInsert(LinkList &L){		//头插法 逆向建立单链表
    LNode *s;
    int x;
    L=(LinkList)malloc(sizeof(LNode));
    L->next=NULL;							//养成好习惯,初始为空链表,防止脏数据
    scanf("%d",&x);							//也可以输入一个链表或者数组
    while(x!=9999){
        s=(LNode*)malloc(sizeof(LNode));
        s->data=x;
        s->next=L->next;
        L->next=s;
        scanf("%d",&x);
    }
    return L;
}
  • 重要应用:单链表的逆置,给定链表逆置的核心逻辑不改变

  • 方法一:新建链表,按顺序取原链表元素,再采用头插法插入新链表

  • 方法二:按顺序取结点插到头结点后,可以让链表原地逆置

2.3.3 双链表

*双链表的定义
  • 双链表可进可退,但存储密度会更低
typedef struct DNode{
    ElemType data;							//数据域
    struct DNode *prior,*next;				//前驱和后继指针
}DNode,*DLinkList;
*双链表的初始化
typedef struct DNode{
    ElemType data;
    struct DNode *prior,*next;
}DNode,*DLinkList;

//初始化一个空的双链表
bool InitDLinkList(DLinkList &L){
    L=(DNode*)malloc(sizeof(DNode));		//分配一个头结点;用DNode*强调为一个结点
    if(L==NULL)								//内存不足,分配失败
        return false;
    L->prior=NULL;							//头结点的prior永远指向NULL
    L->next=NULL;
    return ture;
}

void testDLinkList(){
    DLinkList L;
    InitDLinkList(L);
    //...后续代码...
}

//判断双链表是否为空
bool Empty(DLinkList L){
    if(L->next==NULL)
        return ture;
    else
        return false;
}
*双链表的插入
bool InsertNextDNode(DNode *p,DNode *s){
    s->next=p->next;
    p->next->prior=s;
    s->prior=p;
    p->next=s;
}

//考虑插入位置没有后继结点的情况
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 ture;
}
  • 按位序插入:找到某一个位序的前驱节点,进行后插操作

  • 前插操作:找到给定结点的前驱节点,进行后插操作

*双链表的删除
bool DeleteNextDNode(DNode *p){		//p为删除结点前驱
    if(p==NULL)   
        return false;
    DNode *q=p->next;
    if(q==NULL)   					//p没有后继结点,删除位置q为空
        return false;
    p->next=q->next;
    if(q->next!=NULL)				//删除位置q的后继结点不为空
        q->next->prior=p;
    free(q);
    return true;
}

//销毁一个双链表
void DestoryList(DLinkList &L){
    while(L->next!=NULL)
        DeleteNextNNode(L);
    free(L);						//释放头结点
    L=NULL;							//头指针指向NULL
}
*双链表的遍历
while(p!=NULL){						//后向遍历
    p=p->next;
}

while(p!=NULL){						//前向遍历
    p=p->prior;
}

while(p->prior!=NULL){				//前向遍历(跳过头结点)
    p=p->prior;
}
  • 双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现
  • 平均时间复杂度 : T(n)=O(n)
  • 在双链表的插入和删除操作时,注意边界情况,如插入和删除结点为最后一个结点位置

2.3.4 循环链表

  • 循环单链表:表尾结点的next指针指向头结点
*循环单链表的定义
typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;
*循环单链表的初始化
typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

//初始化一个循环单链表
bool InitList(LinkList &L){
    L=(LNode*)malloc(sizeof(LNode));
    if(L==NULL)
        return false;
    L->next=L;						//头结点next指向头结点
    return ture;
}

//判断循环单链表是否为空
bool Empty(LinkList L){
    if(L->next==L)
        return ture;
    else
        return false;
}	

//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,LNode *p){
    if(p->next==L)
        return ture;
    else
        return false;
}
  • 如果单链表频繁在表头、表尾操作,可以让L指向表尾元素(插入和删除时可能需要修改L)
*循环双链表的初始化
typedef struct DNode{
    ElemType data;
    struct DNode *prior,*next;
}DNode,*DinkList;

//初始化一个循环双链表
bool InitDLinkList(DLinkList &L){
	L = (LNode*)malloc(sizeof(Lnode));
	if(L==NULL){
		return false;  				 
	}
	L->next = L; 					//头结点的prior指向头结点;双链表指向NULL
	L->prior = L;					//头结点的next指向头结点
	return true;
}

//判断循环双链表是否为空
bool Empty(DLinkList L){
	if(L->next == L)				//判断条件与双链表不同
		return true;
	else
		return false;
} 

//判断结点p是否为循环双链表的表尾结点
bool isTail(LinkList L,LNode *p){
	if(p->next == L)
		return true;
	else
		return false;
} 
  • 在使用循环双链表时,不需要考虑边界情况,如插入和删除结点为最后一个结点

2.3.5 静态链表

  • 静态链表:分配一整片连续的内存空间,各个结点集中安置
  • 单链表的指针域:指向下一个结点的指针(地址);静态链表的指针:指向下一个结点的数组下标
  • 0号结点为头结点,游标充当指针,游标为-1表示已经到达表尾
*静态链表的定义
#define MaxSize 10 
struct Node{						//静态链表结构类型的定义 
	ElemType data; 					//存储数据元素 
	int next;						//下一个元素的数组下标 
}; 

void testSLinkList(){
    struct Node a[MaxSize];
    //...后续代码
}

//另一种写法
#define MaxSize 10 
struct Node{		 
	ElemType data;
	int next;	
}SLinkList[MaxSize];
//等价于
#define MaxSize 10 
struct Node{		 
	ElemType data;
	int next;	
}; 
typedef struct Node SLinkList[MaxSize];	//可用SLinkList定义“一个长度为MaxSize的Node型数组”

//另一种写法
#define MaxSize 10      

typedef struct{           
    ELemType data;        
    int next;            
}SLinkList[MaxSize];

void testSLinkList(){
    SLinkList a;
}
  • 注意:SLinkList a 强调a是静态链表;struct Node a 强调a是一个Node型数组、
*静态链表的基本操作
  • 初始化静态链表:把a[0]的next设为-1
  • 查找某个位序的结点(不是数组下标,是各个结点在逻辑上的顺序):从头结点出发挨个往后遍历结点;时间复杂度 T(n)=O(n)
  • 插入位序为i的结点:
    1. 找到一个空的结点,存入数据元素
    2. 从头结点出发找到位序为i-1的结点
    3. 修改新结点的next
    4. 修改i-1号结点的next
  • 删除某个结点:
    1. 从头结点出发找到前驱结点
    2. 修改前驱节点的游标
    3. 被删除节点next设为-2
  • 静态链表:用数组的方式实现的链表
  • 优点:增删操作不需要大量移动元素
  • 缺点:不能随机存取,只能从头结点开始依次往后查

2.3.6 顺序表和链表的比较

  • 逻辑结构:顺序表和链表都属于线性表,都是线性结构

  • 存储结构:

    1. 顺序表——顺序存储;优点:支持随机存取,存储密度高;缺点:大片连续空间分配不方便,改变容量不方便
    2. 链表——链式存储;优点:离散的小空间分配方便,改变容量方便;缺点:不可随机存取,存储密度低
  • 基本操作——创建:

    1. 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源
    2. 静态分配:静态数组,容量不可改变
    3. 动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free())
    4. 链表:只需要分配一个头结点或者只声明一个头指针
  • 基本操作——销毁:

    1. 顺序表:修改 Length = 0

      静态数组——系统自动回收空间

      typedef struct{
          ElemType *data;
          int MaxSize;
          int length;
      }SeqList; 
      

      动态分配:动态数组——需要手动free()

      //创建动态数组
      L.data = (ELemType *)malloc(sizeof(ElemType) *InitSize)
      //销毁动态数组
      free(L.data);
      //malloc() 和 free() 必须成对出现
      
  • 基本操作——增/删:

    1. 顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素
    2. 链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素
  • 基本操作——查:

    1. 顺序表:按位查找为O(1),按值查找为O(n);若表内元素有序,可在O(log2n)时间内找到
    2. 链表:按位查找和按值查找均为O(n)
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值