【考研之数据结构】数据结构线性表好好学习

本文详细介绍了线性表的定义和特性,包括顺序存储和链式存储两种实现方式。顺序存储中,重点讲解了动态分配内存的顺序表,以及插入、删除和查找操作。链式存储部分,主要阐述了单链表的定义、插入、删除、查找等基本操作。此外,还探讨了不同场景下操作的时间复杂度。
摘要由CSDN通过智能技术生成

知识框架

线性表

  1. 顺序存储——顺序表
  2. 链式存储
    1. 单链表
    2. 双链表
    3. 循环链表 1,2,3都是指针实现
    4. 静态链表(借助数组实现)

线性表的定义和基本操作

线性表的定义

线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列。
线性表的特点

  1. 表中元素的个数有限。
  2. 表中元素具有逻辑上的顺序性,表中元素尤其先后次序。
  3. 表中元素都是数据元素,每个元素都是单个元素。
  4. 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间。
    注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不用层面的概念,因此不要混淆。

1. 顺序存储

1.1 线性表的顺序表示的基本操作—初始化

#include <iostream>
#define MaxSize 10  //定义最大长度
using namespace std;

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);
    for(int i = 0; i < MaxSize; i++)
        cout <<L.data[i]<< endl;
    return 0;
}

MaxSize是固定的,当表满了之后就不可更改,(存储空间是静态的
所以来啦顺序表的实现–动态分配

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

Key:动态申请和释放内存空间
C语言中—— malloc、free函数

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

malloc函数返回一个指针,需要强制转换为定义的数据元素类型的指针。 malloc函数的参数,指明要分配多大的连续内存空间
C++可使用new,delete关键字。

#include <stdlib.h>  //包含malloc和free函数

#define InitSize 10 //默认的最大长度
Typedef struct{
	int * data;		//指示动态分配数组的指针
	int MaxSize;	//顺序表的最大容量
	int Length;		//顺序表的当前容量
}SeqList;

void InitList(SeqList &L){
	//用malloc函数申请一片连续的存续空间
	L.data = (int *)malloc(sizeof(int) * InitSize);
	L.MaxSize = InitSize;
	L.Length = 0;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
	int *p = L.date;
	L.date = (int *)malloc(sizeof(int)*(len+L.MaxSize));
	for(int i = 0; i < L.length; i++){
		L.data[i] = p[i];		//将数据复制到新的区域。
	}
	L.MaxSize += len;			//长度增加了len
	free(p); 					//释放原来的内存空间
}

int main() {
	//....
}

//注意realloc函数也可实现,但建议初学者使用malloc和free更能理解背后的过程。

1.2 顺序表的实现

顺序表的特点:

  • 随机访问即在O(1)时间内找到第i个元素。
  • 存储密度高,每个节点只存储数据元素
  • 拓展容量不方便
  • 插入、删除操作不方便,需要移动大量元素
    知识回顾
    在这里插入图片描述

1.3 顺序表—插入、删除

1.3.1 插入操作

代码实现
ListInsert(&L,i,e):插入操作。在表L中的第i个位置插入指定的元素e。

#define MaxSize 10	//定义最大长度
typedef struct{		
	int data[MaxSize];	//本节用静态分配方式实现顺序表,动态也相同
	int length;			//顺序表当前的长度
}SqList;				//顺序表的类型定义

void ListInsert(SqList &L, int i, int e){
	for(int j = L.length; j >= i; j--)
		L.data[j] = L.data[j-1];
	L.data[i-1] = e;			//注意数组从0开始!
	L.length += 1;
}

int main() {
	SqList L;		
	InitList(L);
	//....
	ListInsert(L, 3, 3);
	return 0;
}

可以使插入操作更加健壮 可进行下面改进。

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[j-1] = e;
	L.length += 1;
	return true;
}

好的算法,应该具有“健壮性”
插入操作的时间复杂度

关注最深层循环语句的执行次数与问题规模n的关系
问题的规模n=L.length(表长)
最好是表尾=O(1);
最坏是表头=O(n);
平均:n/2 所以平均时间复制度为=O(n)

1.3.2 删除操作

代码实现

bool ListDelete(SqList &L, int i, int &e){
	if(i < 1 || i > L.length)
		return false;
	e = L.data[i - 1];			//将删除的元素带回来
	for(int j = i; j < L.length; j++)
		L.data[j - 1] = L.data[j];
	L.length--;
	return true;
}

int main() {
	Sqlist L;
	InitList(L);
	//..此处插入一些元素
	int e = -1;  //可以把删除的数据带回来
	if(ListDelete(L, 3, e))
		printf("删除元素为%d",e);
	else
		printf("位置不合法");
	return 0;
}

事件复杂度

最好情况:删除表尾O(1)
最坏情况:删除表头O(n)
平均情况,删除任何一个元素的概率都是相同的,故等差数列求0+1+2+…+(n-1) 故为O(n);

在这里插入图片描述

1.4 顺序表的查找

  • 按位查找
    GetElem(L,i):获得第i个位置的元素
ElemType GetElem(SqList L, i) {
	return L.data[i-1];
}

时间复杂度=O(1);

  • 按值查找
int LocateElem(SqList L, ElemType e) {
	for(int i = 0; i < L.length; i++)
		if(L.data[i] == e)
			return i+1;		//数组的下标为i,位序i+1
	return 0;				//退出循环
}

当为结构体不可以用 ==来判断是否相等,编译都不能通过———考试如果是数据结构的话 可能会有,但是是C语言与程序设计不行。

时间复杂度
((1+n)* n/2)*(1/n)

2. 链式存储

2.1 单链表

单链表的定义

  • 什么是单链表

每个结点除了存放数据元素外,还要存储指向下一个节点的指针
优点:不要求大片连续空间,改造容量方便
缺点:不可随机存取,要消耗一定空间存放指针

  • 用代码定义一个单链表
typedef struct LNode {		//定义单链表的结构类型
	ElemType data;			//每个结点存放一个数据元素
	struct LNode *next;		//指针指向下一个节点
}LNode, *LinkList;
LNode *p = (LNode *)malloc(sizeof(LNode));	//增加新节点:在内存中申请一个节点所需的空间,并用指针p指向这个结点
//LNode *L 可写成LinkList L	声明一个指向单链表第一个结点的指针

LNode * GetElem(LinkList L, int i) {
	int j = 1;
	LNode *p = L -> next;
	if(i==0)
		return L;
	if(i < 1)
		return NULL;
	while(p!=NULL && j<i) {
		p = p -> next;
		j++;
	}
	return p;			//强调是一个单链表  --使用LinkList
}						//强调这是一个结点  --使用LNode *

要表示一个单链表时,只需要声明一个头指针L,指向单链表的第一个结点

  • 两种实现
    • 不带头节点
typedef struct LNode{		//定义单链表结点类型
	ElemType data;			//每个结点存放一个数据结构
	struct LNode *next;		//指针指向下一个节点
}LNode, *LinkList;

//初始化一个单链表
bool InitList(LinkList &L) {
	L = NULL;		//空表,暂时还没有任何节点
	return true;
}

//判断单链表是否为空
bool Empty(LinkList L){
	return (L==NULL);	
}

void test(){
	LinkList L;		//声明一个指向单链表的指针
	InitList(L);	//初始化一个空表
	//...后续代码...
}
- 带头结点 	
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;
}

//判断单链表是否为空(带头结点)
bool Empty(LinkList L) {
	return (L->next == NULL);
}

void test(){
	LinkList L;		//声明一个指向单链表的指针
	InitList(L);	//初始化一个空表
	//...后续代码...
}

->表示左边是指针,现在要提取右边的成员
.表示左边是实体,现在要提取右边的成员
头结点不存数据只是为了操作方便。

在这里插入图片描述

2.2 单链表的基本操作

2.2.1 单链表的插入和删除

在这里插入图片描述

  • 按位序插入(带头结点)
    ListInsert(&L,i,e):插入操作。表示L中的第i个位置上插入指定元素e。(找到i-1位置,插入其后)
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;

//在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e) {
	if(i < 1)
		return false;
	LNode *p;		//指针p指向当前扫描到的节点
	int j = 0;		//当前p指向的第几个结点
	p = L;			//L指向头结点,头结点是第0个结点(不存数据)
	while (p!=NULL && j < i-1) {		//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;
}
  • 按位序插入(不带头结点)
    ListInsert(&L,i,e):插入操作。表示L中的第i个位置上插入指定元素e。(找到i-1位置,插入其后)
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;

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指向第一个结点 (注意不是头结点)
	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.2.2 指定结点的后插操作

typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;

//后插操作:在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->next = p->next;
	p->next = s;
	return true;
}

2.2.3 指定结点的前插操作

前插操作需要传入头指针,否则没法往前遍历,但是可以往后插一个,然后把p 和插入的元素互换位置就解决了问题

//前插操作:在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;
}
//时间复杂度O(1)

2.2.4 按位序删除(带头结点的)

ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除的元素。

typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;

bool ListDelete(LinkList &L, int i, ElemType &e){
	if(i<1)
		return false;
	LNode *p;		//指针p指向当前扫描到的结点
	int j = 0;		//当前p指向的第几个结点
	p = L;			//L指向头结点,头结点是第零个结点(不存数据)
	while(p!=NULL && j < i - 1){ //循环找到第i-1个结点
		p = p->next;
		j++;
	}
	if(p==NULL)		//i值不合法
		return false;
	if(p->next == NULL)	//i-1个结点之后已无其他结点
		return false;
	LNode *q = p->next;		//用q指向被删除的结点
	e = q->data;			//用e返回元素的值
	p->next = q->next; 		//将q结点断开
	free(q);				//释放结点的存储空间
	return true;
}

2.2.5 指定结点的删除

//删除指定的结点 p,就是删除p后面的结点 然后把p后面的值放在p上
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;
}

但是当p是最后一个元素的时候,就只能用土办法,通过头结点找到前驱结点

单链表的局限性:无法逆向检索

2.3 单链表的查找

只讨论带头结点的情况

  • 按位查找

GetElem(L,i):按位查找操作。获取表L中的第i个位置的元素的值。

//按位查找,返回第i个元素(带头结点)
LNode * GetElem(LinkList L, int i){
	if(i<0)
		return NULL;
	LNode *p;	//指针p指向当前扫描到的结点
	int j = 0;	//当前p指向的是第几个结点
	p = L;		//L指向头结点,头结点是第0个结点(不存数据)
	while(p!=NULL && j<i){	//循环到底i个结点
		p = p->next;
		j++;
	}
	return p;
}
  • 按值查找

LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值得元素。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值