线性结构是最简单且最常用的一种数据结构,本章及随后几章要讨论的栈、队列和串都属于线性结构。线性结构的特点是:在数据元素的非空有限集中存在唯一的一个被称作“第一个”的数据元素;存在唯五的一个被称作“最后一个”的数据元素;除第一个元素这外,集合中的每个数据元素均只有一个前驱;除最后一个元素之外,集合中每个数据元素均只有一个后继。
学习要点:
(1)了解线性表的逻辑结构特性,以有线性表的两种存储实出方式。
(2)熟练掌握顺序表和链表的定义和基本运算的实现,并对两者进行比较,了解它们各自的优缺点。
(3)能够应用线性表这种结构解决实际问题。
2.1 线性表的逻辑结构
2.1.1 线性表的类型定义
线性表(Linear_list)简称为表,是最常用且最简单的一种数据结构。一个线性表中包含n个相同类型数据无素A1, A2, A3,......,An的有限序列。
其中:
(1)数据元素的个数n定义为表的长度(n=0时称为空表)。
(2)将非空的线性表(n>0)记作(A1,A2,A3,....An)。
(3)数据元素Ai(1<=i<=n)只是个抽象符号,其具体含义在不同情况下可以不同。
它可以是一个数、或一个符号,也可以是一页书,甚至其他更复杂的信息。
例如:英文字母表 (A,B,C,。。。Z)是一个线性表,表中的每个字母是一个数据元素(结点)。
又如:一副扑克牌的点数也是一个线性表,(2,3,4,。。。10,J,Q, K,A)其中数据元素是每张牌的点数。
在稍微复杂的线性表中,一个数据元素可以由若干个数据项(item)组成。在这种情况下,常把数据元素称为记录(record),
含有大量记录的线性表又称为文件(file)。例如:一个学校的学生基本情况下是一个线性表。
2.1.2 线性表的基本操作
线性表虽然简单,但却是一个相当灵活的数据结构,它的长度可根据需要增长或缩短,即对线性表的数据元素不公可以进行访问,
还可进行插入和删除等。
可以用一个抽象数据类型(Abstract Data Type, ADT)来说明线性表和基本运算,它给出了实例及相关的操作的描述。
假设List表示一个线性表,因为线性表这种数据元素的类型与讨论无关,故可用Elemtype来表示。
常见的线性表的基本运算包括:
(1)Init_List(L)。构造一个空的线性表L,即表的初始化。
(2)List_Length(L)。求线性表L中的结点个数,即求表长。
(3)Get_Node(L,i)。取线性表L中的第i个结点,这里要求1<=i<=List_Length(L).
(4)Location_List(L, x)。在线性表L中查找值为x的结点,并返回该结点在L中的位置。
若L中有多个结点的值和x相同,则返回首次找到结点位置;若L中没有结点的值和x,则返回一个特殊值表示查找失败。
(5)Insert_List(L, x, i)。在线性表L的第i个位置上插入一个值为x的新结点,使原编号为i, i + 1, ...., n的结点变为
编号为i+1, i+2,....n+1的结点,这里1<=i<=n+1,而n是原表L的长度,插入后,表L的长度加1.
(6)Delete_List(L, i)。删除线性表L的第i个结点,使原编号为i + 1, i+2,...., n的结点变为
编号为i,i+1,....n-1的结点,这里1<=i<=n,而n是原表L的长度,删除后表L的长度减1.
(7)ClearList(L, i)。将线性表L重置为空,即置空表。
在计算机内,线性表有两种基本存储结构:顺序存储结构和链式存储结构。
1.线性表顺序存储(顺序表)的实现
【问题描述】
线性表顺序存储的基本运算操作:创建顺序表、在顺序表中某个位置插入某个元素、查找某个元素、在顺序表中删除某个元素等。
下面是顺序表基本运算的演示实例。
【算法描述】
对于线性表顺序存储的运算操作,首先定义存储类型,然后分别设计各种运算的具体函数,最后在主函数中实现顺序表的运算操作。
首先,在主函数中创建包含若干个元素的顺序表,以?结束;调用求长度函数求出顺序表的长度,调用插入函数可以在指定的位置插入元素。
并且可以实现线性表顺序存储的按元素序号和元素内容进行查找等运算操作。
#include "stdio.h"
#include "conio.h"
#define MaxSize 50 //定义线性表可能达到的最大长度
typedef char ElementType;
typedef struct node
{
ElementType data[MaxSize]; //动态分配的一维数组
int length; //当前长度
}SeqList, *list;
//初始化顺序表
void InitList(list l)
{
l->length = 0;
}
//顺序表长度
int LengthList(list l)
{
return l->length;
}
int length(list l)
{
return l->length;
}
//获取顺序表的第i个结点的值
ElementType GetNode(list l, int i)
{
if(i < 1 || i > l->length)
{
printf("get node error");
return '\0';
}
else
{
return l->data[i - 1];
}
}
//查找x在顺序表中的位置
int LocationList(list l, ElementType x)
{
int i = 0;
while(i < l->length && l->data[i] != x)
i++;
if(i == l->length)
return -1; //没找到数据
else
return (i + 1);
}
//向顺序表插入数据
void InsertList(list l, int pos, ElementType x)
{
int j;
if(pos < 1 || pos > l->length + 1)
printf("insert error\n");
else
{
l->length++; //顺序表长度加1
for(j = l->length; j >= pos; j--)
l->data[j] = l->data[j - 1]; //pos位置后面的数据都往后移动一个位置
l->data[pos - 1] = x; //把x插入到pos位置,
}
}
//删除顺序表中的结点
void DeleteNode(list l, int pos)
{
int j;
if(pos < 1 || pos > l->length)
printf("delete error\n");
else
{
for(j = pos; j < l->length; j++)
l->data[j - 1] = l->data[j]; //pos位置后面的数据都往前移一个位置
l->data[l->length - 1] = '\0'; //最后一个元素置空
l->length--; //顺序表的长度减1
}
}
//输出顺序表
void print(list l)
{
int i;
for(i = 1; i < l->length; i++)
printf("%c->",l->data[i-1]);
printf("%c", l->data[l->length-1]);
}
int main()
{
int i = 1, n;
SeqList l;
char ch, x;
InitList(&l);
printf("\n\n\n *************************顺序表演示程序***********\n");
printf(" 请输入你想建立的顺序表的元素,以?结束:");
ch=getchar();
while(ch != '?')
{
InsertList(&l, i, ch);
i++;
ch = getchar();
};
printf(" 你建立的顺序表为:");
print(&l);
printf("\n 顺序表的长度为:%d", l.length);
printf("\n 输入你想查找的元素:");
fflush(stdin); //vs2017不支持
scanf(" %c", &x);
printf(" 你查找的元素为%c序位为%d", x, LocationList(&l, x));
printf("\n 输入你想查找的元素序位:");
scanf(" %d", &n);
printf("\n 你查找的元素为:%c", GetNode(&l, n));
printf("\n 输入你想插入的元素以及序位:<用逗号隔开>:");
scanf(" %c,%d", &x, &n);
InsertList(&l, n, x);
printf("\n 插入后顺序表为:");
print(&l);
fflush(stdin);
printf("\n 请输入你想删除的元素序位:");
scanf(" %d", &n);
DeleteNode(&l, n);
printf("\n 删除后的顺序表为:");
print(&l);
printf("\n 线性表的顺序存储的基本运算操作完毕,谢谢使用!");
return 0;
}
//备注:
//scanf(" %d", &n); scanf函数前面加一个空格的原因是删除缓冲区的第一个字符是回车符或者空格符
//因为前面输入的字符fflush(stdin);并不有起来清空的作用,所以运行时就直接读取缓冲区的回画符了。
//这个和编译器有关系
//运行平台,win10 QT5.13.1 VS2017
//参考链接:
//https://blog.csdn.net/lrgy_zhch/article/details/6268386
//https://blog.csdn.net/weixin_45677047/article/details/104755732
//https://blog.csdn.net/fxwzzbd/article/details/2514042
//https://blog.csdn.net/jonathanlin2008/article/details/5026895
运行结果
2.线性表的链式存储——单链表的实现
【问题描述】
对于线性表还可以采用链式存储结构,在这里通过单链表的基本运算来实现介绍线性
表链式存储方法。
【算法描述】
在设计单链表的各种运算之前,首先定义链式存储的结点类型,链式存储的结点类型
包括数据域 data 和指针域 next 两部分。在实现线性表的链式存储的各种运算操作中,
包括单链表的建立、单链表的输出、向链表中插入新的结点、删除链表中的结点、修改链表中
结点的数据域、链表的转置、释放链表的结点空间等。为了便于实现上述各种运算操作,
在此设计一个主菜单。
#include <stdio.h>
#include <stdlib.h>
typedef char ElementType;
// 结点结构的定义
struct ListNode
{
ElementType data;//数据域
struct ListNode *next;//链域
};
typedef struct ListNode Node;
typedef struct ListNode *Linklist;
//typedef Node *Linklist;
//创建链表
Linklist createList(Linklist head)
{
Linklist ptr, temp;
char ch;
head = (Node *)malloc(sizeof(Node));
if(NULL == head)
{
printf("创建链表错误,分配空间失败");
return NULL;
}
//分配空间成功
head->next = NULL;
ptr = head;
printf("请输入数据元素(输入#号结束。):\t");
scanf(" %c", &ch);
//while((ch = getchar()) != '#')
while(ch != '#')
{
temp = (Node*)malloc(sizeof(Node));
temp->next = NULL;//指针域
temp->data = ch;//数据域
ptr->next = temp;//ptr的下一个节点指向该节点
ptr = temp; //ptr指向该节点,这个时候temp的next为NULL
fflush(stdin);//刷新缓冲区
printf("请输入数据元素(输入#号结束。):\t");
scanf(" %c", &ch);
}
return head->next;
}
//画线函数
void drawLine(char c)
{
int i;
printf("\n");
for (i = 0; i < 70; i++)
printf("%c", c);
printf("\n");
}
//输出链表数据
void printList(Linklist head)
{
Linklist ptr;
ptr = head;
drawLine('-');
printf("单链表显示是:\n");
while(ptr)
{
printf("%3c", ptr->data);
ptr = ptr->next;
}
drawLine('-');
}
//释放链表的结点空间
void freeList(Linklist head)
{
Linklist temp;
while(head)
{
temp = head;
head = head->next;
free(temp);
}
printf("\n 单链表的结点空间释放成功!\n");
}
//向链表中插入新结点
Linklist insertList(Linklist head, Linklist new, ElementType data)
{
Linklist ptr;
ElementType date;
printf("请输入所要插入的数据元素:");
//date = getchar();
scanf(" %c", &date);
new->data = date;
ptr = head;
while(1)
{
if(NULL == head)
{
new->next = head;
head = new;
break;
}
else if(ptr->data == data)
{
new->next = ptr->next;
ptr->next = new;
break;
}
else
{
ptr = ptr->next;
}
}
return head;
}
//删除链表中的结点
Linklist deleteList(Linklist head, ElementType data)
{
Linklist ptr, prv;
ptr = head;
while(1)
{
if(ptr->next == NULL)
{
printf("没有找到所要删除的数据元素!\n");
break;
}
if(head->data == data)
{
head = ptr->next;
free(ptr);
break;
}
prv = ptr;
ptr = ptr->next;
if(ptr->data == data)
{
prv->next = ptr->next;
free(ptr);
break;
}
}
return head;
}
//修改结点的数据域
Linklist modifyList(Linklist head, ElementType data)
{
Linklist ptr;
ElementType date;
ptr = head;//令ptr指向head结点
while(1)
{
if(ptr->next == NULL)
{
printf("没有找到所要修改的数据元素!\n");
break;
}
else if(head->data == data)
{
fflush(stdin);
printf("请输入覆盖 %c 的数据元素:", data);
scanf(" %c", &date);
ptr->data = date;
fflush(stdin);
break;
}
else
{
ptr = ptr->next;//指向下一个结点
}
}
return head;
}
//链表转置
Linklist invertList(Linklist head)
{
Linklist ptr, prv, next;
prv = head;
ptr = prv->next;
prv->next = NULL;
next = ptr->next;
ptr->next = prv;
prv = ptr;
ptr = next;
while(ptr->next)
{
next = ptr->next;
ptr->next = prv;
prv = ptr;
ptr = next;
}
ptr->next = prv;
head = ptr;
return head;
}
int main(void)
{
Linklist head, new;
ElementType data;
int choose;
head = NULL;
drawLine('*');
head = createList(head);
printList(head);
while (1)
{
printf( "\n\n +--------------------- Menu ------------------------+\n" );
printf( "|1.插入 2.删除 3.修改 4.转置 5.释放 0 退出 |\n" );
printf( "+-------------------------------------------------+\n" );
printf("请输入你所要选择的操作 ( 0 -- 5 ):");
scanf( " %d", &choose);
switch (choose)
{
case 0:
exit(0);
break;
case 1:
new = (Node*)malloc(sizeof(Node));
new->next = NULL;
fflush(stdin);
printf("\n 请输入到那个数据元素的后面:");
scanf(" %c", &data);
fflush(stdin);
head = insertList(head, new, data);
printList(head);
break;
case 2:
fflush(stdin);
printf("\n 请输入所要删除的数据元素:");
scanf(" %c", &data);
fflush(stdin);
head = deleteList(head, data);
printList(head);
break;
case 3:
fflush(stdin);
printf("\n 请输入你所要修改的数据元素:");
scanf(" %c", &data);
fflush(stdin);
head = modifyList(head, data);
printList(head);
break;
case 4:
head = invertList(head);
printList(head);
break;
case 5:
freeList(head);
break;
default:
printf("\n 你的选择是非法的!\n");
break;
}
}
return 0;
}
运行结果
3.线性表的应用——约瑟夫算法
约瑟夫回环问题是设有 n 个人围坐在圆桌周围,现从某个位置 m(1≤m≤n)上的人开始报数,报数到 k 的人就站出来。下一个人,
即原来的第 k+1 个位置上的人,又从 1 开始报数,再报数到 k 的人站出来。依次重复下去,直到全部的人都站出来为止。
试设计一个程序求出列序列。
【问题分析】
根据问题提示,可以看出这是一个使用循环链表的问题。因为要不断地出列,采用链表的存储形式能更好地模拟出列的情况。
#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
typedef int Elemtype; //定义数据元素类型
typedef struct Cnode
{
Elemtype data;
struct Cnode *next;
}CNode;
CNode *joseph; //定义一个全局变量
Status createList(CNode *list, int n)
{
CNode *p, *q;
int i;
list = NULL;
for(i = n; i >= 1; i--)
{
p = (CNode*)malloc(sizeof(CNode));
if(NULL == p)
return OVERFLOW; //分配内存失败
p->data = i;
p->next = list;
list = p;
if(i == n)
q = p; //用q指向链表的最后一个结点
}
q->next = list; //把链表的最后一个结点的链域指向链表的第一个结点,构成循环链表
joseph = list; //把创建好的循环链表头指针赋给全局变量
return OK;
}
Status Joseph(CNode *list, int pos, int num, int k)
{
int i;
CNode *p, *q;
if(pos > num)
return ERROR; //起始位置错
if(!createList(list, num))
return ERROR; //循环链表创建失败
p = joseph; //p指向创建好的循环链表
for(i = 1; i < pos; i++)
p = p->next; //p指向pos位置的结点
while(p)
{
for(i = 1; i < k - 1; i++)
p = p->next; //找出第k个结点
q = p->next;
printf(" %d\t", q->data); //输出应出列的结点
if(p->next == p)
{
p = NULL; //删除最后一个结点
}
else
{
p->next = q->next;
p = p->next;
free(q);
}
}
list = NULL;
return OK;
}
int main()
{
int pos, num, k;
CNode *list;
list = NULL; //初始化list
printf("\n 请输入围坐在圆桌周围的人数num:");
scanf(" %d", &num);
printf("\n 请输入第一次开始报数人的位置pos:");
scanf(" %d", &pos);
printf("\n 你希望报数到第几个数的人出列?");
scanf(" %d", &k);
createList(list, num); //创建一个有n个结点的循环链表list;
printf("\n 出列的顺序如下:\n");
Joseph(list, pos, num, k);
printf("\nhello Joseph\n");
return 0;
}
运行结果