前言
线性表是数据结构的一个重要的逻辑结构,前面我们提到,数据结构的逻辑结构是为了算法设计的基础,线性表也广泛应用于很多算法中。
文章中会关于线性表的两种结构进行总结,总体上来说包括有顺序表、链式表。顺序表我们最为熟悉的数组,字符串数组……都属于顺序表;后续的链表,链栈,链式队列……都属于链式表的形式。这篇文章主要通过程序以及程序的注解总结顺序表和链式表的基本的数据操作。
顺序表
线性表的基本操作包括有:
- InitList(L) 初始化
- DestoryList(L) 销毁
- ClearList(L) 清空
- IsEmptyList(L) 空判断
- ListLength(L) 表长
- Locate(L,e) 查找
- GetData(L,i) 获取数据
- InsertList(L,i,e) 插入数据
- DeleteList(L,i,e) 删除数据
程序的开始,我们对结构体指针和结构体变量调用结构体内的成员总结以下,也就是 ‘.’ 和 ‘->’ 的区别:结构体指针用 ‘->’ ,结构体变量用 ‘.’ 。
例如:
typedef struct{
int data;
struct Lnode *next;
}Lnode,*LinkList;
Lnode a; 结构体变量,用a.data、a.next
LinkList s;结构体指针,用s->data、s->next
我们通过C/C++(应用到C++简洁的输入输出等)程序进行总结:
程序如下:
#include<stdio.h>
#include<stdlib.h>
#define OK 0
#define ERROR -1
#define LIST_INIT_SIZE 100
#define LISTINCREMENT 10
typedef int ElemType;
typedef int Status;
//定义一个顺序表的结构体
typedef struct{
//定义一个int类型的指针 功能类似于一位数组
ElemType *elem;
//当前顺序表的长度
int length;
//定义顺序表当前分配的存储容量
int listsize;
}Sqlist;
//初始化顺序表
Status InitList_Sq(Sqlist &L)
{
//定义一个顺序实际上存储数据的长度 n
int n;
L.elem=(ElemType *)malloc(LIST_INIT_SIZE *sizeof(ElemType));
if(!L.elem)
return ERROR;
L.length=0;
L.listsize=LIST_INIT_SIZE;
//为初始化的顺序表赋值
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&L.elem[i]);
L.length = n;
return OK;
}
//顺序表是否为空判断
Status IsEmptyList_Sq(Sqlist L){
if(L.length != 0)
return ERROR;
return OK;
}
//输出顺序表存储数据的实际长度
Status ListLength_Sq(Sqlist L){
return L.length;
}
//查找顺序表中是否含有某数据
Status Locate_Sq(Sqlist L,ElemType e){
//如果能够查找到 返回该元素的位置 从1开始计数
for(int i=0;i<L.length;i++)
if(L.elem[i] == e){
return i+1;
break;
}
//不能查找到 返回异常数据 -1
return ERROR;
}
//获取某一位置上元素
Status GetData_Sq(Sqlist L,int index){
//如果该位置有元素 则返回该元素
if(index <= L.length && index > 0)
return L.elem[index-1];
//不能查找到 返回异常数据 -1 这里的 -1 可能与顺序表中的数据有歧义 这里没有做特殊处理
return ERROR;
}
//在 index 位置上插入一数据 e
Status InsertList_Sq(Sqlist &L, int index, ElemType e){
if(index <= 0 || index > L.length)
return ERROR;
//如果插入的长度大于原来开辟的内存空间大小的情况下 需要再开辟一部分新的空间
//考虑到内存的使用率 再次开辟空间的大小 定义设置为 LISTINCREMENT 10
if(L.length > L.listsize){
ElemType *newbase = (ElemType *)realloc(L.elem,(LIST_INIT_SIZE + LISTINCREMENT) *sizeof(ElemType));
if(!newbase)
exit(ERROR);
L.elem = newbase;
L.listsize += LISTINCREMENT;
}
//这里利用指针来进行数组元素的移动
ElemType *q = &(L.elem[index-1]);
for(ElemType *p = &(L.elem[L.length-1]);p >= q; p--)
*(p+1) = *p;
*q = e;
++L.length;
return OK;
}
//删除顺序表 index 位置上的元素
//也可以查找顺序表中的元素 e进行删除 代码类似 这里不做展示
Status DeleteList_Sq(Sqlist &L, int index){
if(L.length-1 < 0 || index <= 0 || index > L.length)
return ERROR;
//这里利用数组元素进行数据元素的移动
ElemType temp = L.elem[index-1];
for(int i=index-1;i<L.length-1;i++)
L.elem[i] = L.elem[i+1];
--L.length;
return OK;
}
//打印顺序表的数据内容和顺序表目前的长度
Status Print_Sq(Sqlist L){
printf("顺序表目前长度:%d\n",L.length);
printf("顺序表目的数据:");
for(int i=0;i<L.length;i++)
printf("%5d",L.elem[i]);
printf("\n");
return OK;
}
//主函数
int main(){
Sqlist L;
InitList_Sq(L);
Print_Sq(L);
printf("判断顺序表是否为空:%d",IsEmptyList_Sq(L));
printf("\n顺序表目前存储的数据长度:%d",ListLength_Sq(L));
//index 是从1开始计数 同 L.length 相同均从1开始计数
//这里如果搜索不到可以做一个异常处理 可以用条件语句实现 这里不做展示
printf("\n顺序表中元素%d的位置是:%d",5,Locate_Sq(L,5));
//这里如果index位置上出现异常 可以用条件语句实现异常处理 这里不做展示
printf("\n顺序表中%d位置上的元素是:%d\n",5,GetData_Sq(L,5));
//这里如果index位置上出现异常 可以用条件语句实现异常处理 这里不做展示
InsertList_Sq(L,5,99);
Print_Sq(L);
DeleteList_Sq(L,5);
Print_Sq(L);
return OK;
}
/*
8
1 2 3 4 5 6 7 8
*/
程序运行截图:
内存分配方式
- 从全局存储区域分配:内存在程序编译的时候分配好,直到整个程序运行的周期结束才会释放分配好的内存,整个周期内都会存在。例如在程序中声明的全局变量。
- 从栈区分配:主要体现在执行函数的过程中,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆中分配:也成为动态内存分配,程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
我们可以看出文中使用的顺序表的内存分配方式采用的是‘堆分配’的策略,这样对于整个程序而言,会提高开辟内存空间的使用率,是效率最大化。
顺序表的优缺点
优点(一般):
- 存储效率高,存储结构本身已表示数据的逻辑结构,无需额外的存储空间表示逻辑结构。
- 随机访问某个元素。
缺点(一般):
- 插入和删除操作需要移动大量的结点。
- 存储空间分配是静态分配。表的大小要事先确定,这样会造成程序不能适应实际情况的变化。
- 如果事先把表长确定太大,可能会浪费存储空间。
- 如果把表长确定得太小,可能造成数据溢出。
我们可以看出对于一般的静态分配内存空间的顺序表而言,分配固定的存储空间的情况下会造成后面的三个缺点,因此,我们在进行项目实施的过程中一般不会选用静态分配,而是选择内存存储的动态分配,如我们程序中开辟空间的方式一样,这样不仅仅节约内存空间,而且还提高内存空间的使用率。
链表
线性表的链表的部分基本操作包括有:
- InitList(L) 初始化
- CreateList(L) 创建链表
- DestoryList(L) 销毁
- ClearList(L) 清空
- IsEmptyList(L) 空判断
- ListLength(L) 表长
- Locate(L,e) 查找
- GetData(L,i) 获取数据
- InsertList(L,i,e) 插入数据
- DeleteList(L,i,e) 删除数据
基本操作的程序代码如下:
#include<iostream>
#include<cstdlib>
using namespace std;
typedef char Elemtype;
typedef struct LNode{
Elemtype data;
struct LNode *next;
}ListNode,*LinkList;
/*
头插法创建的链表与输入的数据的顺序相反
即倒序的创建链表
另外与链表的逆置操作 可采用头插法创建
入栈的操作 也是与输入的数据顺序相反
*/
//没有头结点的头插法 创建链表
void CreateHead(LinkList &Head)
{
int n;
cout<<"无头结点头插法创建链表"<<endl;
cout<<"请输入创建链表的长度:";
cin>>n;
LinkList p;
Head=NULL;
cout<<"请输入对应长度的字符:";
while(n--)
{
p=new ListNode;
cin>>p->data;
p->next=Head;
Head=p;
}
p=Head;
}
/*
一般均采用具有头结点的这种方法创建链表
*/
//即:有头结点的头插法 创建链表
void CreateReal(LinkList &Real)
{
int n;
cout<<"有头结点头插法创建链表"<<endl;
cout<<"请输入创建链表的长度:";
cin>>n;
LinkList p;
Real=new ListNode;
Real->next=NULL;
cout<<"请输入对应长度的字符:";
for(int i=0;i<n;i++)
{
p=new ListNode;
cin>>p->data;
p->next=Real->next;
Real->next=p;
}
}
//无头结点 输出链表
void Hprint(LinkList L)
{
LinkList p;
p=L;
cout<<"无头结点链表的数据如下:"<<endl;
while(p)
{
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
//有头结点 输出链表
void Rprint(LinkList L)
{
LinkList p;
p=L->next;
cout<<"有头结点链表的数据如下:"<<endl;
while(p)
{
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
/*
采用尾插法
创建的链表的序列和输入数据的顺序相同
*/
//有头结点的尾插法
void CreateOrder(LinkList &L)
{
LinkList q,p;
L=new ListNode;
L->next=NULL;
q=L;
int n;
cout<<"有头结点尾插法创建链表"<<endl;
cout<<"请输入创建链表的长度:";
cin>>n;
cout<<"请输入对应长度的字符:";
while(n--)
{
p=new ListNode;
cin>>p->data;
p->next=NULL;
q->next=p;
q=p;
}
}
//这种建立序列与原始的输入序列相同
void Oprint(LinkList L)
{
LinkList p;
p=L->next;
cout<<"输出链表的数据如下:"<<endl;
while(p)
{
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
//链表的逆置
void ReversalList(LinkList &L)
{
LinkList q,p;
p=L->next;
L->next=NULL;//相当于重新建立一个新的链表
while(p)
{
q=p->next;
p->next=L->next;
L->next=p;
p=q;
}
}
//查找index位置上的元素
int SearchList(LinkList L,int i)
{
LinkList p;
p=L->next;
int j=1;
while(p&&j<i)
{
p=p->next;
j++;
}
if(p)
cout<<i<<"位置上的数据是:"<<p->data<<endl;
else
cout<<"查找异常:Error"<<endl;
}
//在链表的index位置上 插入元素e
void InsertListNode(LinkList &L,int i,Elemtype e)
{
LinkList p;
p=L->next;
int j=1;
while(p&&j<i-1)
{
p=p->next;
j++;
}
if(p)
{
LinkList q;
q=new ListNode;
q->data=e;
q->next=p->next;
p->next=q;
Rprint(L);
}
else
cout<<"插入异常:Error"<<endl;
}
//删除链表index位置上的结点
void DelListNode(LinkList &L,int i,Elemtype &e)
{
LinkList q,p;
p=L->next;
int j=1;
while(p&&j<i-1)
{
p=p->next;
j++;
}
if(p)
{
q=new ListNode;
q=p->next;
e=q->data;
p->next=q->next;
free(q);
Rprint(L);
}
else
cout<<"删除异常:Error"<<endl;
}
//输入链表的实际长度
void LengthList(LinkList L)
{
LinkList p;
p=L->next;
int j=1;
while(p)
{
p=p->next;
j++;
}
cout<<"链表的实际长度是:"<<j-1<<endl;
}
//清空链表的内容
void ClearList(LinkList &L)
{
LinkList p;
while(L->next)
{
p=L->next;
L->next=p->next;
free(p);
}
}
//销毁链表 意味着销毁其内存地址
void DestoryList(LinkList &L)
{
LinkList p;
while(L)
{
p=L;
L=L->next;
free(p);
}//相比较于ClearList而言,把头结点也删除了
}
//主函数
int main(int argc,char **argv)
{
LinkList L;
Elemtype e='u';
CreateReal(L);
Rprint(L);
ReversalList(L);//链表的反转
Rprint(L);
SearchList(L,7);
InsertListNode(L,2,e);//在第二个元素前插入一个元素
DelListNode(L,2,e);//删除第二个元素
LengthList(L);//链表的长度
ClearList(L);
DestoryList(L);
CreateHead(L);
Hprint(L);
DestoryList(L);
CreateOrder(L);
Oprint(L);
return 0;
}
/*
10
H e l l o W o r l d
5
1 2 3 4 5
10
1 2 3 4 5 6 7 8 9 Q
*/
程序运行截图:
结语
没有暖气的世界,就差裹着被子写博客了……
今天下午终于把博客补完。文章的内容除了标明转载和连接的部分,均是博主学习的总结,如果有理解不到或者理解错误的地方请指出,遇到问题可以留言方便交流学习。