文章目录
前言
提示:这里可以添加本文要记录的大概内容:
前言
数据结构是计算机科学中一个重要的基础概念,它研究的是如何组织和管理计算机中的数据。线性表是一种常见的数据结构,它由一组具有相同数据类型 的元素组成,这些元素之间存在着线性关系。
线性表具有许多重要的应用,例如:
- 存储和管理数据
- 实现各种算法
- 构建其他数据结构
在本文中,我们将对线性表的概念、特点、实现和应用进行详细的介绍。
提示:以下是本篇文章正文内容,下面案例可供参考
线性表的定义和基本操作
知识总览
注意:存储结构不同,运算的实现方式也会不同
1.线性表的定义
线性表定义的是数据结构中的逻辑结构
对于图片中关于,如果所有整数按递增次序排列,是线性表吗
答案是否定的,因为从定义中我们可以知道,有限二字,然后整数是可以无穷无尽的
2.线性表的基本操作
这个图大家看看就行
这里建议大家把其中的每个操作都动手写一写,或者在编译器上多写一下,熟悉几遍
对于为什么没有说明各个参数的具体类型?
数据结构书籍中没有说明各个参数的具体类型,是为了提高书籍的通用性、简洁性、灵活性和易读性,并强调对数据结构思想和原理的理解。读者可以通过阅读代码示例和查阅相关资料来了解具体的参数类型。
最后来一个总结的图
顺序表的定义
顺序表------用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中
,元素之间的关系由存储单元的邻接关系来体现。
1.静态分配方式
我们需要知道顺序表的静态分配方式使用一个数组来实现的,数组的长度一旦确定就无法改变!!!
关于图片中把各个数据元素的值设为默认值(可省略的原因)?
因为在顺序表结构中有一个length字段,表示的是顺序表中已有元素的个数,因此只要我们初始化了length就行,不根据length来遍历顺序表的行为都是非法的!!!
代码:
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#define MAXSIZE 10//定义顺序表的最大空间
typedef struct
{
int data[MAXSIZE];//利用静态分配方式实现顺序表
int length;//顺序表中有效元素的个数
}Sqlist;//类型重命名,取名为Sqlist
//初始化一个顺序表
void InitList(Sqlist& L)//利用引用传参
{
for (int i = 0; i < MAXSIZE; i++)//这里对数组元素的初始化不进行也可以,因为到时候遍历利用的也是length字段的值去遍历的
{
L.data[i] = 0;//将数组中的值都初始化为0
}
L.length = 0;
}
int main()
{
Sqlist L;
InitList(L);
return 0;
}
思考:
这里需要注意,length在初始化的时候不能被省略,如果省略了后果很严重,省略了那么length就是一个随机值
改进后的写法:
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#define MAXSIZE 10//定义顺序表的最大空间
typedef struct
{
int data[MAXSIZE];//利用静态分配方式实现顺序表
int length;//顺序表中有效元素的个数
}Sqlist;//类型重命名,取名为Sqlist
//初始化一个顺序表
void InitList(Sqlist& L)//利用引用传参
{
//for (int i = 0; i < MAXSIZE; i++)//这里对数组元素的初始化不进行也可以,因为到时候遍历利用的也是length字段的值去遍历的
//{
// L.data[i] = 0;//将数组中的值都初始化为0
//}
L.length = 0;
}
//遍历顺序表
void Print(const Sqlist&L)
{
for (int i = 0; i < L.length; i++)
{
cout << L.data[i] << " ";
}
cout << endl;
}
int main()
{
Sqlist L;
InitList(L);
Print(L);
return 0;
}
如果数组存满了,那么我们会想到两种解决办法,1.开始的时候就开辟一个足够大的空间,2.进行扩容
对于第一种方法,显然是不可行的,首先程序所需的空间我们在事前是很难预料的,其次如果开辟的空间很大,但是最后使用的空间很小,就会造成空间上很大的浪费!!!对于第二种方法,由于我们采用的是静态分配,空间的大小一定确定就无法更改,所以这里的扩容也进行不下去
2.动态分配方式
通过之前的内容我们可以知道,静态分配方式实现的顺序表对于扩容的时候很困难,那么由此就引出了动态分配方式实现的顺序表,也就是利用数组指针来实现!!!
注意,王道这里用的都是c语言的malloc函数,但是我本人习惯用C++的new关键字,对于两者的内容可以看我的博客内容!!!
结构体定义:
cpp
#define Elemtype int //这里我把类型设置为int类型
typedef struct
{
Elemtype* data;
int capacity;
int length;
}SqList;
动态分配相关代码实现:
cpp
#include <iostream>
using namespace std;
#define InitSize 10
#define Elemtype int //这里我把类型设置为int类型
typedef struct
{
Elemtype* data;
int capacity;
int length;
}SeqList;
void InitList(SeqList& L)
{
L.data = new Elemtype[InitSize];
L.capacity = InitSize;
L.length = 0;
}
void IncreaseSize(SeqList& L, int len)//在原来的基础上多增加len个空间
{
Elemtype* tmp = L.data;
L.data = new Elemtype[len + L.capacity];
memcpy(L.data, tmp,L.length*sizeof(Elemtype));//这里由于元素类型是int类型也就是内置类型,所以我用的是memcpy函数,自定义类型慎用!!!!
delete[] tmp;//释放原空间
L.capacity += len;
}
int main()
{
SeqList L;
InitList(L);
IncreaseSize(L, 5);
return 0;
}
扩容前容量是10
扩容后可以看到容量增加到了15
同时需要注意增容操作往往需要拷贝数据,拷贝数据需要比较大的时间开销
顺序表的特点!!!
小总结:
顺序表的插入和删除
1.顺序表的插入
知识总览
插入操作介绍
插入操作的代码实现
cpp
void InsertSeqList(SeqList& L,int pos,int num)//在指定位序下插入元素num
{
for (int i = L.length; i >= pos; i--)
{
L.data[i] = L.data[i - 1];
}
L.data[pos - 1] = num;
L.length += 1;
}
对边界情况和异常情况的检查,提高代码的健壮性!!!
cpp
#include <iostream>
using namespace std;
#define MAXSIZE 10
typedef struct
{
int data[MAXSIZE];//这里类型用int
int length;
}SeqList;
//初始化
void InitSeqList(SeqList& L)
{
L.length = 0;
}
bool InsertSeqList(SeqList& L,int pos,int num)//在指定位序下插入元素num
{
if (pos<1 || pos>L.length + 1)
{
return false;
}
if (MAXSIZE == L.length)//此时空间已满,不能继续插入
{
return false;
}
for (int i = L.length; i >= pos; i--)
{
L.data[i] = L.data[i - 1];
}
L.data[pos - 1] = num;
L.length += 1;
return true;
}
//遍历顺序表
void Print(const SeqList&L)
{
for (int i = 0; i < L.length; i++)
{
cout << L.data[i] << " ";
}
cout << endl;
}
int main()
{
SeqList L;
InitSeqList(L);
InsertSeqList(L, 1, 100);
Print(L);
return 0;
}
插入操作的时间复杂度
时间复杂度这里图上说的很清楚,个人觉得没有必要继续阐述了
2.顺序表的删除
顺序表的删除操作
可以看到,删除操作是需要把后面元素前移的,这里和插入操作恰好相反!!!
完整代码!!!
cpp
#include <iostream>
using namespace std;
#define MAXSIZE 10
typedef struct
{
int data[MAXSIZE];//这里类型用int
int length;
}SeqList;
//初始化
void InitSeqList(SeqList& L)
{
L.length = 0;
}
bool InsertSeqList(SeqList& L,int pos,int num)//在指定位序下插入元素num
{
if (pos<1 || pos>L.length + 1)
{
return false;
}
if (MAXSIZE == L.length)//此时空间已满,不能继续插入
{
return false;
}
for (int i = L.length; i >= pos; i--)
{
L.data[i] = L.data[i - 1];
}
L.data[pos - 1] = num;
L.length += 1;
return true;
}
//遍历顺序表
void Print(const SeqList&L)
{
for (int i = 0; i < L.length; i++)
{
cout << L.data[i] << " ";
}
cout << endl;
}
bool ListDelete(SeqList& L, int pos, int& e)//删除顺序表pos位置的值,并将被删除元素的值给e
{
if (pos<1 || pos>L.length)
{
return false;
}
e = L.data[pos-1];
for (int i = pos; i < L.length; i++)
{
L.data[i-1] = L.data[i];
}
L.length--;
return true;
}
int main()
{
SeqList L;
int num = 0;
InitSeqList(L);
InsertSeqList(L, 1, 1);
InsertSeqList(L, 2, 2);
InsertSeqList(L, 3, 3);
Print(L);
if(ListDelete(L, 2, num))
cout << num << endl;
Print(L);
return 0;
}
对于图中,如果参数e没加引用符号会怎么样的问题?
如果参数e没加引用符号,那么形参会是实参的拷贝,在main函数中输出的num的值依旧是0
删除操作的时间复杂度
小总结:
顺序表的查找
知识总览:
1.按位查找(简单)
由于顺序表随机存取的特性,显然他的时间复杂度是O(1),其他的没什么太多可说的,都很好理解
代码
这里我的参数加了引用,因为引用共用的是同一块空间,如果不加引用就会多一次拷贝,增加空间的开销
cpp
int GetElem(const SeqList&L,int i)
{
return L.data[i-1];
}
2.按值查找
代码:
cpp
int LocateElem(const SeqList& L, int e)
{
for (int i = 0; i < L.length; i++)
{
if (L.data[i] == e)
return i + 1;
}
return -1;
}
对于结构体类型的元素或是自定义类型的元素不能使用==运算符,c++中需要使用运算符重载才可以使用,c语言不能使用
解决办法
注意:
时间复杂度
小总结:
单链表的定义
带头结点和不带头结点
这里就是说明链式存储的存储密度比顺序存储低
1.代码定义一个单链表
改进后,单链表的定义
cpp
typedef struct LNode
{
int data;//数据域
struct LNode* next;//指针域
}LNode,*LinkList;
cpp
struct LNode
{
int data;//数据域
struct LNode* next;//指针域
};
2.不带头节点的单链表
代码:
cpp
#include <iostream>
using namespace std;
typedef struct LNode
{
int data;//数据域
struct LNode* next;//指针域
}LNode,*LinkList;
//初始化
bool InitList(LinkList& L)
{
L = NULL;
return true;
}
//判断单链表是否为空
bool empty(LinkList L)
{
return (L == NULL);
}
int main()
{
LinkList L;
InitList(L);
cout << empty(L) << endl;
return 0;
}
3.带头节点的单链表
注意头结点不存储数据
cpp
#include <iostream>
using namespace std;
typedef struct LNode
{
int data;//数据域
struct LNode* next;//指针域
}LNode,*LinkList;
//初始化
bool InitList(LinkList& L)
{
L = new LNode;//分配一个头结点,或者用c语言的方式(LNode*)malloc(sizeof(LNode))
if (L == NULL)//内存不足,分配失败
return false;
L->next = NULL;
return true;
}
//判断单链表是否为空
bool empty(LinkList L)
{
return (L ->next== NULL);
}
int main()
{
LinkList L;
InitList(L);
cout << empty(L) << endl;
return 0;
}
小结:
单链表的插入和删除
1.按位序插入带头节点
cpp
#include <iostream>
using namespace std;
typedef struct LNode
{
int data;//数据域
struct LNode* next;//指针域
}LNode,*LinkList;
//初始化
bool InitList(LinkList& L)
{
L = new LNode;//分配一个头结点,或者用c语言的方式(LNode*)malloc(sizeof(LNode))
if (L == NULL)//内存不足,分配失败
return false;
L->next = NULL;
return true;
}
//在第i个位置插入元素e(带头节点)
bool ListInsert(LinkList& L, int i, int e)
{
if (i < 1)
{
return false;
}
int j = 0;//记录当前是第几个结点
LNode* p = L;//临时结点p
while (p != NULL && j < i-1)//找到第i-1个结点的位置
{
p = p->next;
j++;
}
if (p == NULL)
return false;
LNode* s = new LNode;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
void Print(LinkList L)
{
LNode* p = L;
while (p->next != NULL)
{
p = p->next;
cout << p->data << " ";
}
cout << endl;
}
//判断单链表是否为空
bool empty(LinkList L)
{
return (L ->next== NULL);
}
int main()
{
LinkList L;
InitList(L);
ListInsert(L,1,1);
ListInsert(L,2,2);
ListInsert(L,3,3);
ListInsert(L,4,4);
Print(L);
return 0;
}
2.按位序插入不带头节点
代码:
cpp
#include <iostream>
using namespace std;
typedef struct LNode
{
int data;//数据域
struct LNode* next;//指针域
}LNode, * LinkList;
//初始化(不带头节点)
bool InitList(LinkList& L)
{
L = NULL;
return true;
}
//在第i个位置插入元素e(不带头节点)
bool ListInsert(LinkList& L, int i, int e)
{
if (i < 1)
{
return false;
}
if (i == 1)
{
LNode* s = new LNode;
s->data = e;
s->next = L;
L = s;
return true;
}
LNode* p = L;
int j = 1;
while (p != NULL && j < i - 1)
{
p = p->next;
j++;
}
if (p == NULL)
return false;
LNode* s = new LNode;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
void Print(LinkList L)
{
LNode* p = L;
while (p != NULL)
{
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
//判断单链表是否为空
bool empty(LinkList L)
{
return (L == NULL);
}
int main()
{
LinkList L;
InitList(L);
ListInsert(L, 1, 1);
ListInsert(L, 2, 2);
ListInsert(L, 3, 3);
ListInsert(L, 4, 4);
Print(L);
return 0;
}
3.指定结点的后插操作
代码:
cpp
bool InsertNExtNode(LNode* p, int e)
{
if (p == NULL)
return false;
LNode* s = new LNode;
if (s == NULL)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
4.指定结点的前插操作
代码:
cpp
bool InsertPriorNode(LNode* p, int e)
{
if (p == NULL)
return false;
LNode* s = new LNode;
if (s == NULL)
return false;
s->next = p->next;
p->next = s;
s->data = p->data;
p->data = e;
return true;
}
5.指定结点的删除操作
代码:
cpp
bool DeleteNode(LNode* p)
{
if (p == NULL)
return false;
LNode* q = p->next;
p->data = q->data;
p->next = q->next;
delete q;
return true;
}
小结:
总结
在本文中,我们对线性表的概念、特点、实现和应用进行了详细的介绍。
线性表的特点是:
- 元素之间存在着线性关系
- 每个元素只有一个前驱和一个后继
线性表的实现方式主要有两种:
- 顺序表
- 链表
线性表的应用非常广泛,例如:
- 存储和管理数据
- 实现各种算法
- 构建其他数据结构
通过学习本文,读者应该能够:
理解线性表的概念和特点
掌握线性表的实现方法
了解线性表的应用