数据结构——浅谈链式表示线性表
一、链表
前面我也讲到了顺序表示的优缺点,并且我们在写入代码时也可以发现,虽然利用顺序表进行查找时非常便
但是在插入删除时进行了大量的元素移动,特别是在数据量特别巨大时,插入删除一个元素耗费的时间是
常巨大的,所以我们有了另一种存储表示方法, 就是链式存储。而采用链式存储结构的线性表称为线性链表。
链表分类:
- 从实现角度看,链表可分为
- 动态链表
- 静态链表
- 从链接方式的角度看,链表可分为
- 单链表
- 循环链表
- 双链表
二、代码实现
从单链表开始:
对于链表的代码实现首先应该知道对于单链表的一个 结点 的构成。
可以看见每一个单链表的结点至少需要一个存放数据的数据域,和一个存放下一个结点的指针域,指针域的目的是因为链表是利用一组任意的存储单元存储数据的,这样就不像顺序表那样可以只需要通过表的首地址就可以找到整个表,而是需要在表的每一个结点中加入下一个结点的起始位置,这样才能将表连接起来(其实这也就是我理解的链表),同样的链表中的每个结点只有一个指针域,就将这种链表称为单链表。所以链表结点的定义就需要定义数据域和指针域。
typedef struct Node //结点类型定义
{
ElemType data;
struct Node* next;
}Node, * LinkList; //LinkList为结构指针类型
建立单链表
对于创建单链表我们有两种建立方法,分别是
头插法建表
算法大概是:
- 从一个空表开始,重复读入数据,生成新结点
- 将读入数据存放到新结点的数据域中
- 将新结点插入到当前链表的表头结点之后,直至读入结束标志为止
LinkList CreateFromHead() //带头结点的//
{
LinkList L;
Node* s;
int flag = 1;
//设置标志,初值为1,当输入“#”时,flag为0,建表结束
L = (LinkList)malloc(sizeof(Node));//为头结点分配空间
L->next = NULL;
while (flag) {
char c = getchar();
if (c != '#') //为读入的字符分配存储空间
{
s = (Node*)malloc(sizeof(Node));
s->data = c; s->next = L->next; L->next = s;
}
else
flag = 0;
}
}
第二种插入方法是尾插法
算法其实和头插法差别不大只是将头插法中“将新结点插入到当前链表的表头结点之后”换为“将新结点插入到当前链表的最后一个结点之后”
LinkList CreateFromTail()
{
LinkList L;
Node* r, * s;
int flag = 1;
L = (Node*)malloc(sizeof(Node));//为头结点分配空间
L->next = NULL; r = L; //r指针始终动态指向链表的当前表尾
while (flag) //输入“#”时flag为0,建表结束
{
char c = getchar();
if (c != '#') {
s = (Node*)malloc(sizeof(Node));
s->data = c;
r->next = s;
r = s;
}
else
{
flag = 0;
r->next = NULL;
}
}
}
现在来看看单链表的查找操作
和顺序表一样的链表的查找常见的也分为按序号和按内容两种,只不过在链表查找这一块由于单链表是通过前一个结点得到下一个结点的指针,所以进行查找时一般只能从头开始将需要的结点前的每一个结点都给遍历一遍。
按序号查找
算法:
- 从单链表的头指针L出发,从头结点(L->next)开始顺着链域扫描
- 用指针p指向当前扫描到的结点,初值指向头结点(pL->next)
- 用j做记数器,累计当前扫描过的结点数(初值为0)
- 当j = i 时,指针p所指的结点就是要找的第i个结点
Node* Get(LinkList L, int i)
{
Node* p;
LinkList p = L;
int j = 0; //从头结点开始扫描
while ((p->next != NULL) && (j < i))
{
p = p->next; //扫描下一结点
j++; //已扫描结点计数器}
if (i == j) return p; //找到了第i个结点
else return NULL;//找不到,i≤0或i>n
}
}
按值查找
算法:
- 从单链表的头指针L出发,从头结点(L->next)开始顺着链域扫描
- 用指针p指向当前扫描到的结点,初值指向头结点(pL->next)
- 用 j 做记数器,累计当前扫描过的结点数(初值为0)//可以看见这里其实和按序号查找没什么区别
- 每一次p指向一个新的结点时,将p->data中的值与我们需要查找的值进行比较,相同输出 j 值,并返回结点的地址p,不同就指向下一个结点(p=p->next)
Node* Locate(LinkList L, ElemType key)
{
Node* p;
p = L->next; //从表中第一个结点比较
int j = 0;
while (p != NULL)
if (p->data != key)
{
p = p->next;
j++;
}
else
{
printf("查找的值在链表第%个位置", j);
//其实在链表中找在第几个位置感觉还是有点多余了,怎么用到,但是不写上又不舒服。。
break; //找到结点key,退出循环
}
return p;
}
单链表插入
对于链表的插入无论是什么类型的链表,永远记住在进行操作的时候p指针永远永远别先动,因为一旦先对p进行操作就会找不到p的相邻结点,导致链表数据丢失或者是链表直接损坏。
void InsList(LinkList L, int i, ElemType e)
{
Node* p, * s;
p = L;
int k = 0;
while (p != NULL && k < i - 1)
//找到第i-1个数据元素的存储位置,使指针Pre指向它
{
p = p->next; k = k + 1;
}
if (k != i - 1)
{
printf("插入位置不合理!");
return;
}
s = (Node*)malloc(sizeof(Node)); //为e申请新的结点
s->data = e; //将待插入结点的值e赋给s的数据域
s->next = p->next;
p->next = s;
}
单链表删除
对于单链表的删除也是一样的,永远别先对p进行操作!!!
欲在带头结点的单链表L中删除第i个结点,则首先要通过计数方式找到第i-1个结点并使p指向第i-1个结点,而后删除第i个结点并释放结点空间。
int DelList(LinkList L, int i, ElemType* e)
{
Node* p, * r; p = L; int k = 0; //寻找被删除结点i的前驱
while (p->next != NULL && k < i - 1)
{
p = p->next; k = k + 1;
}
if (k != i - 1) //即循环是因为p->next=NULL而跳出的
{
printf("所删结点位置i不合理");
return -1;
}
r = p->next;
p->next = p->next->next; //删除结点r
free(r); //释放被删除的结点所占的内存空间
}
求单链表的长度
这一个其实是在整个单链表算法中算最简单的了
int ListLength(LinkList L)
{
Node* p;
p = L->next;
int j = 0;//用来存放单链表的长度
while (p != NULL)
{
p = p->next; j++;
}
return j;
}
当然对于链表的还有很多操作比如求两个集合的差这些我就不过多叙述了。
其实对于线性表一个最典型的利用应该可以说是悲剧文本了,尝试着做一些
问题描述:
你有一个破损的键盘。键盘上所有的键都可以正常工作,但有时候Home键或者End键会自动按下。你并不知道键盘存在这一问题,而是专心打稿子,甚至连显示器都没打开。当你打开显示器后,展现在你面前的是一段悲剧文本。你的任务是在打开显示器之前计算出这段悲剧文本。
输入包含多组数据。每组数据占一行,包含不超过100000个字母、下划线、字符“[”或者“]”。其中字符“[”表示Home键,“]”表示End键。输入结束标志为文件结束符(EOF)。输入文件不超过5MB。对于每组数据,输出一行,即屏幕上的悲剧文本。
样例输入:
This_is_a_[Beiju]_text
样例输出:
BeijuThis_is_a_text