数据结构-线性表

数据结构系列文章目录

数据结构三要素——逻辑结构、数据的运算、存储结构(物理结构)
小提:储存结构不同,运算的实现方式不同。


从此博客开始到后续的一段时间,都是计蒙本人在为考研做准备。不定时输出用于复习。(加粗字体需要重点理解,每句话后面可能会给出简单解释。)

文章目录

  • 数据结构系列文章目录
  • 逻辑结构
  • 线性表的基本操作(运算)
  • 储存结构
    • 顺序表(顺序储存)
      • 实现-静态分配
      • 实现-动态分配
      • 动态申请一片连续的储存空间
      • 使用C语言初始化表:关键代码
      • 顺序表特点:(常考概念题)
      • 顺序表插入操作
      • 顺序表删除操作
      • 顺序表查找操作
    • 单链表(链式储存)
      • 定义一个单链表
      • 不带头结点,带头结点
      • 头插法(设立头结点)
      • 尾插法:顺序一致
      • 插入
      • 按序号查找 时间复杂度 O(n)
      • 按值查找 O(n)
      • 插入结点 (如果是第i个位置) O(n)
      • 删除结点(如果是第i个位置)O(n)
    • 双链表
      • 类型描述
      • 插入操作
      • 删除操作
    • 循环链表
    • 静态链表


逻辑结构

线性表是具有相同数据类型的n(n≥0)个数据元素的有限 序列,其中n为表长,当n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为:
请添加图片描述

相同:每个数据元素所占的空间一样大。

序列:有次序。(线性序列关系)

List = (D,R)
 其中:D = {ai | ai∈ElemSet,i=1,2,…n,n≥0}
 R = {N}, N = {< ai-1 , ai > | ai-1 , ai ∈D , i = 2,3,…n}
  • ai是线性表中的“第i个”元素线性表中的位序(位序从1开始
    数组下标从0开始)
  • a1是表头元素;an是表尾元素。

特点:在数据元素的非空有限集中

  • 有且仅有一个开始结点。
  • 有且仅有一个终端结点。
  • 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

图解:来源于互联网
请添加图片描述

线性表的基本操作(运算)

  • InitList(&L) 初始化表,构造一个空的线性表L(分配内存空间)
  • ListLength(L) 求长度,返回线性表中数据元素个数
  • GetElem(L,i,&e) 取表L中第i个数据元素赋值给e(按位查找)
  • LocateElem(L,e) 按值查找,若表中存在一个或多个值为e的结点,返回第一个找到的数据元素的位序,否则返回一个特殊值。
  • ListInsert(&L,i,e) 在L中第i个位置前插入新的数据元素e,表长加1。
  • ListDelete(&L,i,e) 删除表中第i个数据元素,e返回其值,表长减1。
&” —— 对参数的修改结果需要“带回来”。

储存结构

顺序表(顺序储存)

  • 用“物理位置”相邻来表示线性表中数据元素之间的逻辑关系。
  • 根据线性表的顺序存储结构的特点,只要确定了存储线性表的起始位置,线性表中任一数据元素都可随机存取,所以,线性表的顺序存储结构是一种随机存取的存储结构。

实现-静态分配

#define MaxSize 10 //定义最大长度
typedef struct{
   ElemType data[MaxSize]; //用静态的“数组”存放数据元素
   int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)

在C语言中一般用sizeof(ElemType)来得出一个数据元素的大小
如:sizeof(int) = 4B

所以静态分配的储存空间,大小为
MaxSize*sizeof(ElemType)

实现-动态分配

#define InitSize 10 //顺序表的初始长度
typedef struct{
    ElemType *data; //指示动态分配数组的指针
    int MaxSize; //顺序表的最大容量
    int length; //顺序表的当前长度
} SeqList; //顺序表的类型定义(动态分配方式)

动态申请一片连续的储存空间

C:

L.data = (ElemType *) malloc (sizeof(ElemType) * InitSize);

C++:

L.data=new ElemType[InitSize]

使用C语言初始化表:关键代码

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

int main{
SeqList L;
InitList(L);
.....
}

顺序表特点:(常考概念题)

  • 随机访问,即可以在 O(1) 时间内找到第 i 个元素。
  • 存储密度高,每个节点只存储数据元素
  • 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
  • 插入、删除操作不方便,需要移动大量元素

顺序表插入操作

ListInsert(&L,i,e):插入操作。在表L中的**第i个位置上(位序)**插入指定元素e。

编码/考试时建议考虑算法的要求-(健壮性,可读性等)

bool ListInsert(SqList &L, int i, ElemType e)
{
	if (i<1 || i>L.length + 1) //判断位置是否有效
	{
		//位置无效
		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++;        //长度加一
	return true;
}

时间复杂度(平均时间复杂度):O(n) n/2

顺序表删除操作

不需要将删除元素赋值给e

bool  DeleteList(SqList &L, int i)
{
	if (i<1 || i>L.length)
	{
		return false;
	}
	for (int j = i; j <= L.length - 1; j++)//位置i之后元素依次前移覆盖
	{
		L.data[j - 1] = L.data[j];
	}
	L.length--;   //线性表长度减1
	return true;
}

将删除元素赋值给e

bool  DeleteList(SqList &L, int i,int &e)
{
	if (i<1 || i>L.length)
	{
		return false;
	}
	e=L.data[i-1]   //数组下标从0开始
	for (int j = i; j <= L.length - 1; j++)//位置i之后元素依次前移覆盖
	{
		L.data[j - 1] = L.data[j];
	}
	L.length--;   //线性表长度减1
	return true;
}

时间复杂度(平均时间复杂度):O(n) (n+1)/2

顺序表查找操作

按位查找:获取表L中第i个位置的元素的值

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

时间复杂度:O(1)

按值查找: 在表L中查找具有给定关键字值的元素

int LocateElem(SeqList L,ElemType e){
   for(int i=0;i<L.length;i++)
    if(L.data[i]==e){
     return i+1; //如果数组下标为i的元素值等于e,返回其位序i+1
    }
   return 0; //在循环中没找到值,说明查找失败。
}

平均时间复杂度 = O(n) (n+1)/2
注意:基本数据类型:int、char、double、float 等可以直接用运算符“==”比较,而结构体不能直接用

概念简洁点:
每个结点中只存放数据元素
优点:可随机存取,存储密度高,缺点:要求大片连续空间,改变容量不方便

单链表(链式储存)

每个结点除了存放数据元素外,还要存储指向下一个节点的指针,不要求大片连续空间,改变容量方便。

定义一个单链表

typedef 关键字 —— 数据类型重命名

typedef struct LNode
{
	ElemType data;//数据域
	struct LNode *next;//指针域   指向下一个结点
}LNode,*LinkList;

不带头结点,带头结点

不带头结点初始化方法:

bool InitList(LinkList &L){
 L=NULL;   //防止脏数据
 return true;
}

理解:对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑
空表判断: L==NULL;

带头结点初始化:头结点不存数据,只是为了操作方便

bool InitList(LinkList &L){
 L = (LinkList)malloc(sizeof(LNode));  //分配一个头结点
 if(L==NULL){       //内存不足,失败判断,输出false
   return false;
 }
 L->next=null;
 return true;
}

空表判断: L->next==NULL;

头插法(设立头结点)

头插法:实现较为简单,但是与输入顺序的数据不一致。

LinkList HeadCreatList(LinkList &L) //头插法建立链表
{
    LNode *s;int x;
	L = (LinkList)malloc(sizeof(LNode));   //初始化空表,申请一个头结点(创建头结点)
	L->Next = NULL;        //头指针为空
	scanf("%d",&x);      //输入值
	for (int i = 0; i <99; i++)     
	{
		s = (LNode*)malloc(sizeof(LNode));  //p指向新申请的结点
		s->data=x;
		s->next=L->next;
		L->next=s;    //新结点插入表中。
		scanf("%d",&x);      //输入值
	}
	return L;
}

时间复杂度: O(n)

尾插法:顺序一致

LinkList List_TailInsert(LinkList &L){ 
   int x;   //设ElemType为整型
   L=(LinkList)malloc(sizeof(LNode));  //建立头结点
   LNode *s,*r=L;  //r为表尾指针
   scanf("%d",&x);  //输入结点的值
  while(x!=99){ //输入99表示结束
    s =(LNode *)malloc(sizeof(LNode));
    s->data=x;
    r->next=s;
    r=s; //r指向新的表尾结点
   scanf("%d",&x);
  }
  r->next=NULL; //尾结点指针置空
   return L;
}

时间复杂度:O(n)

插入

按位序插入(带头结点)

bool ListInsert(LinkList &L , int index , ElemType e){ // 插入 
    if(index < 1){
        return false;
    }
    LNode *p = L; // 指向头节点 
    int j = 0 ;
    while(p != NULL && j < index-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;
} 

按位序插入(不带头结点)

bool ListInsert(LinkList &L , int index , ElemType e){/// 不带头节点的 
    if(index < 1 ) 
        return false;
    if(index == 1){ // 不带头节点的要特别判段1这个位置,因为这是链表是空的 ,不能进行插入操作
        LNode *s = (LNode*)malloc(sizeof(LNode));
        s->data = e ;
        s->next = NULL;
        L = s  ; // 把头节点带回去 
        return true;
    }
    LNode *p = L;    
    int j = 1 ;
    while(p!=NULL && j < index-1){
        p = p->next ;
        j++;
    } 
    // 为插入的数据申请内存 
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next ;
    p->next = s;
    return s;    插入成功
} 

按序号查找 时间复杂度 O(n)

LNode * GetElem(LinkList L , int index){
    if(index < 1)   
        return NULL;
    LNode *p = L;
    int j = 0 ;
    while(p != NULL && j < index){  
        p = p->next;
        j++;
    }
    if(p == NULL)
        return NULL; // 没找到 
    return p;
}

按值查找 O(n)

LNode * LocateElem(LinkList L , ElemType e){ // 按值查找 
    LNode *p = L->next;
    while(p != NULL && p->data != e){
        p = p->next;
    }
    if(p == NULL)
        return NULL;
    return p;
}

插入结点 (如果是第i个位置) O(n)

先调用GetElem(L,i-1)查找第i-1个结点;

p=GetElem(L,i-1;
s->next=p->next;
p->next=s;

算法开销主要在查找上,如果在给定的结点后插入,则时间复杂度为O(1)

删除结点(如果是第i个位置)O(n)

p=GetElem(L,i-1;
q=p—>next;
p->next=q-next;
free(q);    //释放空间

算法开销主要在查找上,如果在给定的结点后删除,则时间复杂度为O(1)

双链表

出现:单链表无法逆向检索。

类型描述

typedef struct DNode
{
	ElemType data;//数据域
	struct DNode *prior, *next;//前驱,后继指针域
}DNode,*DLinkList;

插入操作

在p指的结点后插入结点*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);   //释放空间

循环链表

循环单链表:表尾结点的next指针指向头结点。 (算法与单链表几乎一致,不同为如果在表尾进行操作,则操作步骤不同。)
循环双链表:表头结点的 prior 指向表尾结点;表尾结点的 next 指向头结点。

循环双链表插入
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);

静态链表

借助数组来描述线性表的链式储存结构。

静态链表的插入删除与动态链表的相同,只需要修改指针,而不需要移动元素。

优点:增、删 操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变。

你可能感兴趣的:(数据结构,数据结构,线性表)