数据结构学习笔记——第2章 线性表

2 线性表

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

2.1.1 线性表的定义

  • 线性表是具有相同数据类型的n(n>=0)个数据元素有限序列,其中n为表长,当n=0时线性表是一个空表。
  • 若用L命名线性表,则其一般表示为 L=(a1, a2, …, ai, ai, …, an)
    • a1是唯一的“第一个”数据元素,又称表头元素
    • an是唯一的“最后一个”数据元素,又称为表尾元素
    • 除第一个元素外,每个元素有且仅有一个直接前驱
    • 除最后一个元素外,每个元素有且仅有一个直接后继
  • 线性表的特点如下:
    • 表中元素的个数有限
    • 表中元素具有逻辑上的顺序性,表中元素有其先后次序
    • 表中元素都是数据元素,每个元素都是单个元素
    • 表中元素的数据类型相同,这意味着每个元素占有相同大小的存储空间
    • 表中元素具有抽象性,即讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容
    • 线性表是一种逻辑结构,表示元素之间一对一相邻的关系

2.1.2 线性表的基本操作

  • InitList(&L):初始化表。构造一个空的线性表
  • DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间
  • LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素
  • GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值
  • ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e(前插)
  • ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值
  • PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值
  • Empty(L):判空操作。若L为空表,则返回TRUE,否则返回FALSE
  • Length(L):求表长。返回线性表L的长度,即L中数据元素的个数
  • 注意
    • 基本操作的实现取决于采用哪种存储结构,存储结构不同,算法的实现也不同
    • “&”表示C++中的引用调用。在C中采用指针的指针也可以达到同样的效果

2.2 线性表的顺序表示

2.2.1 顺序表的定义

  • 线性表的顺序存储又称顺序表
  • 用一组地址连续的存储单元一次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻
  • 特点:逻辑顺序与其物理顺序相同
  • L=(a1, a2, …, ai, …, an)
    线性表的顺序存储结构
  • 注意:
    • n与Max的区别
    • sizeof(ElemType)是每个数据元素所占用存储空间的大小
    • 线性表中元素的位序是从1开始的,而数组中元素的下标是从0开始的
  • 顺序表最主要的特点是随机访问,即了通过首地址和元素序号可在时间O(1)内找到指定的元素
  • 顺序表的存储密度高,每个结点只存储数据元素
  • 顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素
静态分配
#define MaxSize 50              //定义线性表的最大长度
typedef struct{               
	ElemType data[MaxSize];     //顺序表的元素
	int length;                 //顺序表的当前长度
}SqList;                        //顺序表的类型定义
  • 在静态分配时,由于数组的大小和空间事先已经固定,一旦空间占满,再加入新的数据将会产生溢出,进而导致程序崩溃
动态分配
#define InitSize 100            //表长度的初始定义
typedef struct{                 
	ElemType *data;             //指示动态分配数组的指针
	int MaxSize, length;        //数组的最大容量和当前个数
}SeqList;                       //动态分配数组的顺序表的类型定义
  • 在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储数组空间的目的,而不需要为线性表一次性地划分所有空间
  • 动态分配语句
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);    //C语言
L.data = new ElemType[InitSize];                          //C++
  • 注意:动态分配并不是链式存储,他同样属于顺序存储结构

2.2.2 顺序表上基本操作的实现

插入操作
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;
}
  • 注意:区别顺序表的位序和数组下标
  • 最好情况:在表尾插入(即i=n+1),元素后移语句将不执行,时间复杂度为O(1)
  • 最坏情况:在表头插入(即i=1),元素后移语句将执行n次,时间复杂度O(n)
  • 平均情况:n/2,时间复杂度为O(n)
删除操作
bool ListDelete(SqList &L, int i, ElemType &e){
	if(i < 1 || i > L.length)            //判断i的范围是否有效
		return false;
	e = 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),需移动除第一个元素外的所有元素,时间复杂度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的元素值等于e,返回其位序i+1
	}
	return 0;                              //退出循环,说明查找失败
}
  • 最好情况:查找的元素就在表头,仅需比较一次,时间复杂度为O(1)
  • 最坏情况:查找的元素在表尾(或不存在)时,需要比较n次,时间复杂度O(n)
  • 平均情况:(n+1)/2,时间复杂度为O(n)

2.3 线性表的链式表示

2.3.1 单链表的定义

  • 线性表的链式存储又称单链表
  • 通过一组任意的存储单元来存储线性表中的数据元素
  • 对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
  • 单链表结点结构:
    单链表结点结构
  • 结点类型的描述如下:
typedef struct LNode{          //定义单链表结点类型
	ElemType data;             //数据域
	struct LNode *next;        //指针域
}LNode, *LinkList;
  • 利用单链表可以解决顺序表需要大量连续存储单元的缺点,但单链表附加指针域,也存在浪费存储空间的缺点
  • 单链表是非随机存取的存储结构。在查找某个特定的结点时,需要从表头开始遍历,以此查找
  • 通常用头指针来表示一个单链表。此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,成为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。
    带头结点的单链表
  • 头结点和头指针的区分:无论有无头结点,头指针始终指向链表的第一个结点,而头结点是带头结点的链表的第一个结点,结点内通常不存储信息
  • 引入头结点,可以带来两个优点
    • 由于第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需进行特殊处理
    • 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表he非空表的处理也就得到了统一

2.3.2 单链表上基本操作的实现

采用头插法建立单链表

采用头插法建立单链表

LinkList List_HeadInsert(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->next = s;                           //将新结点插入表中,L为头指针
		scanf("%d", x);
	}
	return L;
}
  • 读入数据的顺序与生成链表中的元素的顺序是相反的
  • 每个节点的插入时间为O(1),设单链表长为n,则总时间复杂度为O(n)
采用尾插法建立单链表

采用尾插法建立单链表

  • 增加一个尾指针r,使其时钟指向当前链表的尾结点,以节省遍历的时间
LinkList List_TailInsert(LinkList &L) {        //正向建立单链表(尾插法)
	int x;                                     //设元素类型为整型
	L = (LinkList)malloc(sizeof(LNode));
	LNode *s;
	LNode *r = L;                              //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;
}
  • 因为附设了一个尾指针,故时间复杂度和头插法的相同
按序号查找结点值
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大于表长则返回NULL
}
  • 时间复杂度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
}
  • 时间复杂度O(n)
插入结点操作
p = GetElem(L, i-1);      //查找插入位置的前驱结点
s->next = p->next;        //修改指针域,顺序不能颠倒
p->next = s;
  • 查找第i-1个元素,时间复杂度为O(n);在第i-1个元素后面插入新结点,时间复杂度O(1);总时间复杂度为O(n)
插入到给定结点*p前
s->next = p->next;        //修改指针域,顺序不能颠倒
p->next = s;
temp = p->data;           //交换数据域部分
p->data = s->data;
s->data = temp;
  • 已知第i个元素地址,因此不需要遍历单链表;在第i个元素前面插入新结点,时间复杂度O(1);总时间复杂度为O(1)
删除结点操作
p = GetElem(L, i-1);      //查找删除位置
q = p->next;              //将q指向被删除结点
p->next = q->next;        //将*q结点从链中“断开”
free(q);                  //释放结点的存储空间
  • 时间复杂度为O(n)
删除给定结点*p
q = p->next;              //令q指向*p的后继结点
p->data = p->next->data;  //和后继结点交换数据域
p->next = q->next;        //将*q结点从链中“断开”
free(q);                  //释放后继结点的存储空间
  • 时间复杂度O(1)
求表长操作
int count = 0;
LNode *p = L;
while(p->next != NULL){
	count++;
	p = p->next;
}
  • 时间复杂度O(n)

2.3.3 双链表

  • 为了方便方位某个节点的前驱结点,引入了双链表,双链表中有两个指针prior和next,分别指向亲戚借点和后继结点
    双链表示意图
typedef struct DNode {              //定义双链结点类型
	ElemType data;                  //数据域
	struct DNode *prior, *next;     //前驱和后继指针
}DNode, *DLinkList;
双链表的插入操作
  • 在双链表中p所指的结点之后插入结点*s
    双链表的插入操作
s->next = p->next;         //将结点*s插入到结点*p之后
p->next->prior = s;
s->prior = p;
p->next = s;
  • 顺序不唯一,但第1、2步必须在第4步之前
  • 时间复杂度O(1)
双链表的删除操作

双链表的删除操作

p->next = q->next;
q->next->prior = p;
free(q);                 //释放结点空间
  • 时间复杂度O(1)

2.3.4 循环链表

循环单链表

循环单链表

  • 在循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针
  • 循环单链表任何一个位置上的插入和删除操作都是等价的,无需判断是否是表尾
  • 仅设尾指针效率会更高。若设尾指针为r,r->next即为头指针,对于表头和表尾的操作都只需要O(1)的时间复杂度
循环双链表

循环双链表

  • 在循环双链表L中,某结点*p为尾结点时,p->next==L
  • 当循环双链表为空时,其头结点的prior域和next域都等于L

2.3.5 静态链表

  • 静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next
  • 指针是结点的相对地址(数组下标),又称游标
  • 静态链表以next==-1作为其结束的标志
  • 需要预先分配一块连续的内存空间
#define MaxSize 50
typedef struct {
	ElemType data;
	int next;
}SLinkList[MaxSize];
  • 常在一些不支持指针的高级语言中使用

2.3.6 顺序表和链表的比较

  • 存取(读写)方式
    • 顺序表可以顺序存取,也可以随机存取,只需一次访问
    • 单链表只能从表头顺序存取元素,需访问i次
  • 逻辑结构与物理结构
    • 采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻
    • 采用链式存储时,逻辑上相邻的元素,物理存储位置则不一定相邻,对应的逻辑关系是通过指针链接来表示的
  • 查找、插入和删除操作
    • 对于按值查找
      • 顺序表无序时,两者的时间复杂度均为O(n)
      • 顺序表有序时,可采用折半查找,时间复杂度为O(log2n)
    • 对于按序号查找
      • 顺序表支持随机访问,时间复杂度仅为O(1)
      • 链表的平均时间复杂度为O(n)
    • 插入、删除操作
      • 顺序表,平均需移动半个表长的元素,且时间复杂度为O(n)
      • 链表只需修改相关结点的指针域即可,结点指针已知O(1),结点指针未知O(n)
    • 链表的每个结点都带有指针域,故存储密度不够大
  • 空间分配
    • 顺序存储需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后补大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然不会溢出,但是扩充需要大量移动元素,操作效率低;而且若是内存中没有更大块的连续存储空间,则会导致分配失败
    • 连输存储在需要时分配结点空间即可,高效方便,但指针要使用额外空间
在实际中应该怎样选取存储结构?

怎样选择线性表的存储结构

三个常用操作
最值
  • 顺序表
int min = L[0];
int max = L[0];
for(int i = 0; i < n; i++) {
	if(min > L[i])
		min = L[i];
	if(max < L[i])
		max = L[i];
}
  • 链表
int min = p->next->data;
int max = p->next->data;
for(; p != NULL; p = p->next) {
	if(min > p->data)
		min = p->data;
	if(max < p->data)
		max = p->data;
}
逆序
  • 顺序表
int i = 0;
int j = n-1;
while(i < j) { 
	temp = L[i];
	L[i] = L[j];
	L[j] = temp;
	i++; j--;
}
  • 链表
while(p->next != r) {
	temp = p->next;
	p->next = temp->next;
	temp->next = r->next;
	r->next = temp;
}
归并
  • 顺序表
int i = 0, j = 0, k = 0;
while(i < A.length && j < B.length) {
    if(A.data[i] <= B.data[j])
        c.data[k++] = A.data[i++];
    else
        c.data[k++] = B.data[j++];
}
while(i < A.length)
    c.data[k++] = A.data[i++];
while(j < B.length)
    c.data[k++] = B.data[j++];
c.length = k;
  • 链表
while(p->next != NULL && q->next != NULL) {
	if(p->next->data < q->next->data) {
		r->next = p->next;
		p->next = p->next->next;
		r = r->next;
	} else {
		r->next = q->next;
		q->next = q->next->next;
		r = r->next;
	}
	if(p->next != NULL)
		r->next = p->next;
	if(q->next != NULL)
		r->next = q->next;
	free(p); free(q);
} //注意:这么合并之后r并不指向尾结点
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值