王道数据结构 | 第二章 线性表

王道中这章主讲了线性表的定义、基本操作、顺序表示、链式表示。下方内容主分了文字部分和代码部分,便于记忆和整理。
在901中这章的要求集中在链表的基础操作中,应用题大概会出问答题。
【当前每一小节的应用题待做,先把选择题过完,整本书预计1个月。每日15-20页】

7.3115-30页:线性表、顺序表+练习题
8.131-45页:单链表、双链表、循环链表、静态链表
8.246-62页:链表部分35道选择题review

第二章 线性表

文字内容

  1. 线性表的特点:元素个数 关系 形式 数据类型 表达含义
  2. 线性表、顺序表、链表不同之处
  3. 顺序表的特点 (相对于链表而言,一句话概括)
  4. 顺序表 静态分配优缺点
  5. 顺序表动态分配的过程 和 链式存储的区别
  6. 顺序表的优缺点
  7. 链式存储与顺序表的不同 (讨论:存取+删除+插入操作)
  8. 单链表在存储空间上的优缺点
  9. 单链表的存储结构是什么,具体说明一下
  10. 单链表如何查找特定结点
  11. 头结点和头指针的关系
  12. 引入头结点有什么优点(两个一致)
  13. 双链表解决了单链表什么问题?如何解决的?
  14. 循环单链表与单链表的区别【其插入 删除 算法与单链表一样吗】
  15. 为什么会说:有时对循环单链表仅设尾指针,可以使得操作效率更高
  16. 循环双链表与循环单链表的定义不同之处,为空表时是怎样的

  1. 元素个数有限;元素之间具有先后次序即顺序性;均为数据元素,每个元素都是单个元素;数据类型都相同,每个元素都占有相同的存储空间;具有抽象性,元素可以表达很多内容,但主要考虑的是元素之间的逻辑关系
  2. 线性表是一种逻辑结构,表示元素之间一对一的相邻关系。而顺序表和链表是存储结构。顺序表是顺序存储形式,而链表是链式存储,一般前者在代码中使用数组实现,后者使用指针实现。
  3. 逻辑顺序与其存储的物理顺序相同。
  4. 对数组静态分配时,因为数组的大小和空间事先已经固定,所以一旦空间占满,再加入新数据就会产生溢出,进而导致程序崩溃。
  5. 动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,从而达到数组存储空间的目的,而不需要为线性表一次性地划分所有的空间。但动态分配仍属于顺序存储结构,物理结构没有改变,依然是随机存取的方式,只是分配的空间大小可以在运行时动态分配。
  6. 优:可进行随机访问通过首地址和元素序号可以在O(1)时间内找到指定元素存储密度高,每个结点只存储数据元素;缺:元素的插入和删除需要移动大量的元素,插入平均移动n/2,删除平均移动(n-1)/2;顺序存储分配需要一段连续的存储空间,不够灵活。
  7. 顺序表的存储位置可以用一个简单直观的公式表示,它可以随机存取表中任意元素,但插入和删除操作需要移动大量元素;链式存储线性表时, 不需要使用地址连续的存储单元,即不要求逻辑顺序与物理位置顺序一致,它通过“链”建立元素之间的逻辑关系,因此插入和删除操作不需要移动元素,只需要修改指针,但也会失去顺序表可随机存取的优点。
  8. 优点:解决顺序表需要大量连续存储单元的缺点;缺点:附加的指针域,也存在浪费存储空间的缺点。
  9. 单链表的存储结构是非随机存取的,是由于单链表的元素离散地分布在存储空间中,所以不能直接找到表中的某个特定的结点。
  10. 从表头开始遍历,依次查找。
  11. 头指针是一定存在的,只不过区别在于单链表是否带头结点,从而导致头指针指向结点不同 不管带不带头结点,头指针都始终指向链表的第一个节点,而头结点是带头结点的链表的第一个结点,结点内通常不存储信息。
  12. ①由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需进行特殊处理。②无论链表是否为空,其头指针都是指向头结点的非空指针(空表中的头结点的指针域为NULL),因此空表和非空表的处理也得到了一致。
  13. 单链表只有一个指向后继的指针,使得单链表只能从前往后依次遍历,如果要访问前驱只能从头开始,访问前驱的时间复杂度为O(n),为了克服这个缺点,引入了双链表。双链表中有两个指针prior 和 next分别指向了直接前驱和直接后继
  14. 单链表的尾部指向NULL,循环单链表的尾部指向头结点,从而整个链表形成一个环。在单链表中只能从头结点开始往后顺序遍历整个list但是循环单链表可以从表中的任意一个结点开始遍历整个list。【几乎一样,这里你明白表尾不同就行,他操作是一样的就算是表尾,它的前驱结点指向它的next不就还是尾部结点指向头结点,所以过程中无需判断是否为表尾。】
  15. 若是设的头结点,对在表尾插入元素需要O(n)的时间复杂度,而若是设的是尾指针r,r->next即为头指针,对在表头或表尾插入元素都只需要O(1)的时间复杂度。
  16. 头结点的prior指针指向尾结点,尾结点的next指向头结点。当循环双链表为空表时,head->prior = head; head->next = head;

  1. 顺序表是用一组【】存储线性表中的【】,从而使得逻辑上相邻的两个元素在【】也相邻。
  2. 线性表的链式存储又称【】,它是指通过一组【】来存储线性表中的【】。为了建立数据元素之间的【】关系,对每个链表结点,除存放元素自身的信息之外,还需要存放【】。
  3. 通常用【】来标识一个单链表,指出链表的起始地址,其为【】时是一个空表。
  4. 头结点是【】,其数据域【】。单链表带头结点时,头指针head指向【】,不带头结点时,head指向【】。
  5. 代码中设p为指向链表结点的结构体指针,则*p表示【】,访问数据域代码为【】;其中.的左侧为【】,->的左侧为【】,得到下一个结点的存储数据【】。
  6. 静态链表是用【】来描述线性表的链式存储结构,结点也有指针域next和数据域data,与之前的链表不同的是,这里的指针是【】又称游标。和顺序表一样,静态链表也要预先【】。
  7. 顺序表和链表的比较
存取方式
逻辑结构和物理结构
插入、查找、删除操作
空间分配

  1. 地址连续的存储单元、数据元素、物理位置上
  2. 单链表、任意的存储单元、数据元素、线性、一个指向其后继的指针
  3. 头指针head or L、NULL
  4. 单链表在第一个数据结点前附加的一个结点、可以不设置任何信息也可以记录表长等信息、头结点、第一个数据结点。
  5. 这个结点本身、p->data or (*p).data、普通结构体变量、结构体指针、p->next->data or (*(*p).next).data
  6. 数组、结点在数组中的相对地址(数组下标)、分配一块连续的内存空间。
存取方式顺序表顺序存取也可以随机存取;链表只能从表头开始依次顺序存取。
逻辑结构和物理结构顺序表中,逻辑上相邻的两个元素,对应的物理结构也相邻;链表就不一定了,其对应的逻辑关系是通过指针链表来表示的。
插入、查找、删除操作按值查找:顺序表无序,O(n),有序时,O(logn)【二分法】;按位置查找:顺序表支持随机访问O(1),链表的平均时间复杂度为O(n)。删除插入:顺序表移动半个表长的元素,链表只需要修改相关节点的指针域即可。
空间分配顺序存储在静态分配的情况下,装满就不能扩充,若再加入新元素,就会内存溢出,因此需要实现分配足够大的存储空间,这个很难把控,过大造成闲置,过小造成溢出;顺序表的动态存储虽然存储空间可以扩充,但需要移动大量元素,造成操作效率降低,而且若内存中没有更大块的连续存储空间,会导致分配失败;链表存储的节点空间只在需要时申请分配,操作灵活、高效。由于链表的每个结点都需要指针域,存储密度不够大。

选择(仅错题)缩放:150%

2.2.3 试题精选

在这里插入图片描述
【错误答案】A or C
【思路】随机存取:可以直接通过地址或下标在常数时间内访问数据的存取方式。而且,线性表 顺序存储结构使用数组实现,所以是 可以随机存取的存储结构;
【补充】
索引存取:通过索引(类似目录或指针)来查找和访问数据的存取方式。e.g.数据库的索引、B树、B+树、哈希表。
链式结构应该是顺序存取的存储结构;

在这里插入图片描述【错误答案】A
【思路】随机存取:和下标有关的选项 肯定是和n无关 所以BD先排除 A :需要遍历整个顺序表 因为不能根据值索引 C:正确 故C

在这里插入图片描述
【错误答案】A
【思路】
Ⅰ SeqList.data[i] SeqList.data[i-1]
Ⅱ len <= MaxSize 时 data[len] = newnode
Ⅲ 需要移动后面len-1个结点往前移位 和n有关
Ⅳ 移动len-i个节点 和n有关
所以ⅠⅡ时间复杂度O(1) 选C
在这里插入图片描述
【错误答案】A
【思路】顺序表的效率高于链表:随机存取
Ⅰ 符合随机存取的能力,链表还需要顺序查找到指向i结点的指针才能输出它的数据域
Ⅱ 交换值,虽然交换动作二者比较不出,但是链表还需要将指针移动到位置3所以还是顺序表效率高
Ⅲ 二者都需要遍历,差不太多。所以C

2.3.7 尸体精选 试题精选

做题的时候确实已经可以直接把我脑子入土了,错了好多,没事巩固的知识也多

在这里插入图片描述
【错误答案】C
【思路】 结点内的存储单元地址≠结点的存储空间
结点内的存储单元地址是指:在每个节点内部,数据域和指针域的存储单元地址,肯定是连续的。所以A
结点的存储空间是不连续的。链式存储:链表 逻辑顺序和物理位置不一致

在这里插入图片描述
【错误答案】B
【思路】
Ⅰ顺序存储结构直接理解为数组,也可以链式结构吧 静态链表 ×
Ⅱ 删除表尾元素,需要知道这个尾结点的前驱结点,但在单链表中需要遍历,所以还是和表长有关的 ×
Ⅲ 带头节点的单循环链表为空表时head指向自己
Ⅳ 对,我当时好像想成了数组可以折半查找所以O(logn)
Ⅴ 队列 先进先出原则 进:头插法 出:应该是带尾指针的循环双链表吧,不然尾结点的前驱找不到哇 ×
判断完了,才发现如果Ⅱ不对,那只能选BCD Ⅴ肯定对 所以D
【补充知识】循环单链表表示队列:对Ⅴ的判别错误,是对单链表实现队列 分析有误:
在这里插入图片描述

在这里插入图片描述
【错误答案】B
【思路】有序单链表,首先将一维数组有序的话是O(nlogn)各种排序一般好像是快速排序,然后分配空间建立链表O(n)所以D
在这里插入图片描述
【错误答案】C or B 选了B 我感觉C太笼统了:)
【思路】记住就好,不增加头结点也会标识出结点中首结点的位置,所以C。单链表中增加一个头结点的目的是方便运算的实现。
在这里插入图片描述【错误答案】B
【思路】读题啊大哥 线性表:所以分为链表和顺序表 如果顺序表 50 链表 0 所以D
在这里插入图片描述
【错误答案】D 无语死了
【思路】有序单链表插入一个新结点,最低 头部 O(1) 最高 尾部 O(n) 所以B
在这里插入图片描述
【错误答案】D
【思路】循环单链表 欸 为空表 head->next == head 所以?头结点的指针域与L的值相等
【错误原因】L *L分别不清楚
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
【错误答案】C
【思路】末尾插入结点:能快速指向尾部就节省时间,尾指针或者循环+头指针; 删除结点:能够随时遍历整个链表 ,要求最节省时间:方便找前驱,所以A
在这里插入图片描述
【错误答案】D
【思路】头尾相接的时间复杂度为O(1) 循环单链表 前一个链表的尾部改为指向后一个链表的头部 后一个链表的尾部改为指向前一个链表的头部。所以,都指向尾部比较合适,因为都要改变他们的尾部结点指向,所以B。
在这里插入图片描述
【错误答案】B
【思路】看下图思路 选D
在这里插入图片描述

在这里插入图片描述
【错误答案】B
【思路】看下图所以选C
在这里插入图片描述

代码内容

注意:一般可以忽略边界条件判断,变量定义内存分配等细节,主要体现算法思想。所以这里更要多过几遍,以防考场上对于细节过于细究或者模糊浪费时间。纸质考试≠上机考!!!

顺序表

静态分配一维数组的顺序表存储结构描述:
#define MaxSize 50
typedef struct {
	ElemType data[MaxSize]; // 顺序表的元素
	int length; //顺序表当前长度
}SqList;//顺序表类型定义
动态分配一维数组的顺序表存储结构描述:
#define InitSize 100
typedef strcut{
	ElemType *data;//动态分配数组的指针
	int MaxSize,length;//数组的最大容量和当前个数
}SeqList;
//C ++ 动态分配语句
L.data = new ElemType(InirSize);
//C 动态分配语句
L.data =(ElemType*)malloc(sizeof(ElemType)*InitSize);
顺序表的初始化InitList(SqList &L) or SeqList &L

静态只需要设置length 动态对maxsize length data都需要进行设置或分配。

//声明一个顺序表SqList 静态分配
void InitList (SqList &L){
	L.length = 0;
}
//动态分配
void InitList(SeqList &L){
	L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);
	L.length = 0;
	L.MaxSize = InitSize;
}
顺序表的插入操作InsertList()

已知:顺序表L 新元素e 插入为序i ----> 数组下标为i-1【如果搞不清,则判断插入位置是否合法和for循环细节上就会搞错】

bool ListInsert(SqList &L, int i, ElemType e) {
    // 判断i是否有效,i的取值范围应该在1到L.length+1之间
    if (i < 1 || i > L.length + 1) { // 插入位置不合法
        return false;
    }
    // 当前存储空间已满
    if (L.length >= MaxSize) {
        return false;
    }
    // 从最后一个元素开始,依次向后移动,直到第i个位置
    for (int j = L.length; j >= i; --j) {
        L.data[j] = L.data[j - 1];
    }
    // 在第i个位置插入新元素
    L.data[i - 1] = e;
    // 长度加1
    L.length++;
    return true;
}
顺序表的删除操作ListDelete()

已知:顺序表 删除位置 删除元素的返回

bool ListDelete(SqList &L , int i , ElemType &e){
	//判断i
	if(i < 0 || i > L.length) return false;
	//不用判断当前长度
	e = L.data[i-1];
	for(int j = i-1 ; j < L.length-1; j++){
		L.data[j] = L.data[j+1];//j+1不能越界 越length
	}
	L.length--;
	return true;
}
顺序表的按值查找 LocateElem()

已知:顺序表 查找元素 返回位序

int LocateElem(SqList &L , ElmeType e){
    int i;
	for(i = 0 ; i < L.length ; i++){
		if(L.data[i] == e) return i+1;
	}
	return 0;
}

单链表 默认带头结点

存储结构描述LNode LinkList
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode , *LinkList;
带头结点の单链表的初始化InitList()
bool InitList(LinkList &head){
	head = (LNode*)malloc(sizeof(LNode));
	head->next = NULL;
	return true;
}
不带头结点の单链表的初始化InitList()
bool InitList(LinkList &head){
	head = NULL;
	return true;
}
单链表求表长Length()
//带头结点
int Length(LinkList L){
	int len = 0;
	LNode *p = L->next;
	while(p){
		p=p->next;
		len ++;
	}
	return len;
}

or

int Length(LinkList L){
	int len = 0;
	LNode *p = L;
	while(p->next){
		p = p->next;
		len++;
	}
	return len;
}
单链表按序号查找结点GetElem()

已知:L、第i个结点 如果i超过了表长返回NULL【书中写的小于表长???那不就可以找到的吗】

LNode *GetElem(LinkList L , int i){
	LNode *p = L;
	int j = 0;
	while(p != NULL && j < i){//我写成了p->next && j<i 错的家人们!!
		p=p->next;
		j++;
	}
	return p;
}
单链表按值查找表结点LocateElem()

已知:给定值e 当没有这个值返回NULL

LNode *LocateElem(LinkList head , ElemType e){
	LNode *p = head->next;
	while( p != NULL && p->data != e){
		p = p->next;
	}
	return p;
}
单链表插入结点操作ListInsert()【后插】

已知:L、第i个结点处插入、data 为 e

bool ListInsert(LinkList &head , int i , ElemType e){
	LNode *newNode = (LNode*)malloc(sizeof(LNode));
	newNode->data = e;
	//移动到i-1的位置
	LNode *p = head;
	int j = 0;
	while(p != NULL && j<i-1){
		p = p->next;
	}
	//!!!!!!判定i是否合法
	if(p == NULL) return false;
	//插入结点
	newNode->next = p->next; //颠倒的话需要多增加一个指针变量存储被覆盖的p->next;
	p->next = newNode;
	return true;
}

前插操作,是p在位置i处的这个结点之前插入newNode.书上的思路结点上仍然是后插操作,结束后将newNode与p内的数据域进行交换。

单链表删除结点操作ListDelete()

已知:L、位置i、要返还的ElemType e

bool ListDelete(LinkList &head , int i, ElemType &e){
	LNode *p = head;
	//移动至i-1处
	int j = 0;
	while(p != NULL && j<i-1){
		p = p->next;
		j++;
	}
	//判定i是否合法 比【插入】多一个条件
	if(p ==NULL || p->next ==NULL)return false;
	//删除操作 因为要释放被删除结点 所以新开一个指针
	LNode *q = p->next;
	e = q->data;
	p->next = q->next;
	free(q);
	
	return true;
}

扩展不知道他在干啥,遇到了再说吧【坑 located in 电子版48页】

头插法建立单链表
LinkList List_HeadInsert(LinkList &head){
	LNode *newNode ; int x;
	head = (LNode*)malloc(sizeof(LNode));
	head->next = NULL;
	scanf("%d",&x);
	while(x != 9999){//输入9999表示结束
		newNode = (LNode*)malloc(sizeof(LNode));
		newNode->next = head->next;
		newNode->data = x;
		head->next = newNode;
		scanf("%d" ,&x);
	}
	return head;
}
尾插法建立单链表
LinkList List_TailInsert(LinkList &head){
	LNode *newNode; int x;
	head = (LNode*)malloc(sizeof(LNode));
	head->next = NULL;
	LNode *p = head;//尾指针
	scanf("%d",&x);
	while(x!=9999){
		newNode = (LNode*)malloc(sizeof(LNode));
		newNode->next = NULL;
		newNode->data = x;
		p->next = newNode;
		p = p->next;
		scanf("%d" ,&x);
	}
	return head;
}

书上

LinkList List_TailInsert(LinkList &L){
	int x;
	L = (LNode*)malloc(sizeof(LNode));
	LNode *s , *r;//r为尾指针 s为新结点
	scanf("%d" ,&x);
	while(x!=9999){
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		r->next = s;
		r = s;
		scanf("%d",&x);
	}
	r->next = NULL;
	return L;
}

双链表

要疯了本来以为就结束了结果才到35!!!!

存储结构描述DNode DLinkList
typedef struct DNode{
	ElemType data;
	strcut DNode *prior;
	strcut DNode *next;
}DNode, *DLinkList;
插入操作 结点p后插入结点s
	p->next->prior = s;
	s->prior = p;
	s->next = p->next;
	p->next = s;	
删除操作 结点p的后继结点q
	q->next->prior = p;
	p->next = q->next;
	free(q);

静态链表

存储结构描述

以next == -1 作为其结束的标志。删除插入与动态一致,只需要修改指针,不需要移动元素,没有单链表方便。

#define MaxSize 50 
typedef struct {
	ElemType data;
	int next;
}SLinkList(MaxSize);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值