线性结构有这样的特点,在数据元素的非空有限集中:(1)存在唯一的一个被称作“第一个”的元素;(2)存在唯一的一个被称作“最后一个”的数据元素;(3)除第一个之外,集合中的每个数据元素均只有一个前驱;(4)除最后一个之外,集合中的每个数据元素均只有一个后继。
1、介绍
线性表(linear List)是最常用而且最简单的一种数据结构,简单地说,一个线性表是n个数据元素的有限序列。在稍微复杂的线性表中,一个数据元素可以由若干个数据项(item)组成。这种情况下,通常把数据元素称为记录(record),含有大量记录的线性表又称为文件(file)。线性表中的数据元素可以是各种各样的,但同一线性表中的元素必定具有相同特性,即属同一对象,相邻数据元素之间存在着序偶关系。
若将线性表记为(a1,...,ai-1,ai,ai+1,...,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,...,n-1时,ai有且仅有一个直接后继,当i=2,3,...,n时,ai有且仅有一个直接前驱。若将线性表中元素的个数n(n>=0)定义为线性表的长度,n=0时称为空表。在非空表中的每个数据元素都有一个确定的位置,如a1是第一个数据元素,an是最后一个数据元素,ai是第i个数据元素,称i为数据元素ai在线性表中的位序。线性表是一个相当灵活的数据结构,它的长度可根据需要增长或缩短,即对线性表的数据元素不仅可以进行访问,还可以进行插入和删除等。
2、线性表的抽象数据类型定义
ADT List{
数据对象:D={ai | ai∈ElemSet, i=1,2,……,n, n≥0}
数据关系:R1={<ai-1,ai> | ai-1,ai∈D, i=2,……,n}
基本操作:
InitList(&L)
操作结果:构造一个空的线性表L。
DestroyList(&L)
初始条件:线性表L已存在。
操作结果:销毁线性表L。
ClearList(&L)
初始条件:线性表L已存在。
操作结果:将L重置为空表(清空)。
ListEmpty(L)
初始条件:线性表L已存在。
操作结果:若L为空表,则返回TRUE,否则返回FALSE。
ListLength(L)
初始条件:线性表L已存在。
操作结果:返回L中数据元素个数。
GetElem(L,i,&e)
初始条件:线性表L已存在,1≤i≤ListLength(L)。
操作结果:用e返回L中第i数据个元素的值。
LocateElem(L,e,compare())
初始条件:线性表L已存在,compare( )是数据元素判定函数。
操作结果:返回L中第1个与e满足关系compare( )的数据元素的位序。若这样的数据元素不存在,则返回值为0。
PriorElem(L,cur_e,&pre_e)
初始条件:线性表L已存在。
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败,pre_e无定义
NextElem(L,cur_e,&next_e)
初始条件:线性表L已存在。
操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无定义。
ListInsert(&L,i,e)
初始条件:线性表L已存在,1≤i≤ListLength(L)+1。
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1。
ListDelete(&L,i,&e)
初始条件:线性表L已存在且非空,1≤i≤ListLength(L)。
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1。
ListTraverse(L,visit())
初始条件:线性表L已存在。
操作结果:依次对L的每个数据元素调用函数visit()。一旦visit()失败,则操作失败。
}ADT List
对上述定义的抽象数据类型线性表,还可以进行一些更复杂的操作,例如,将两个或两个以上的线性表合并成一个线性表;把一个线性表拆开成两个或两个以上的线性表;重新复制一个线性表等。
3、线性表的表示和实现
3.1 线性表的顺序表示和实现
3.1.1 线性表的顺序表示介绍
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素。假设线性表的每个元素需占用l个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置,则线性表中第i+1个数据元素的存储位置LOC(ai+1)和第i个数据元素的存储位置LOC(ai)之间满足关系:LOC(ai+1)=LOC(ai)+l。一般来说,线性表的第i个元素ai的存储位置为LOC(ai)=LOC(a1)+(i-1)*l,其中,LOC(a1)是线性表的第一个数据元素a1的存储位置,通常称作线性表的起始位置或基地址。
线性表的这种机内表示称作线性表的顺序存储结构或顺序映像(sequential mapping),通常,称这种存储结构的线性表为顺序表。它的特点是为表中相邻的元素ai和ai+1赋以相邻的存储位置LOC(ai)和LOC(ai+1)。也就是说,以元素在计算机内“物理位置相邻”来表示线性表中数据元素之间的逻辑关系,每个数据元素的存储位置都和线性表的起始位置相差一个和数据元素在线性表中位序成正比的常数。因此,只要确定了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构,见图1。
图1 线性表的顺序存储结构示意图
3.1.2 顺序表的基本操作
在线性表的顺序存储结构中,很容易实现线性表的某些操作,如随机存取第i个数据元素,只需注意C语言中数组的下标是从0开始,因此,若L是SqlList类型的顺序表,则表中第i个数据元素是L.elem[i-1]。下面着重说明线性表的插入和删除操作。
线性表的插入操作是指在线性表的第i-1个数据元素和第i个数据元素之间(在这个位置插入元素之后,新插入的元素就成了第i个元素)插入一个新的数据元素,就是要使长度为n的线性表(a1,...,ai-1,ai,ai+1,an)变成长度为n+1的线性表(a1,...,ai-1,b,ai,ai+1,...,an)。数据元素ai-1和ai之间的逻辑关系发生了变化。在线性表的顺序存储结构中,由于逻辑上相邻的数据元素在物理位置上也是相邻的,因此,除非i=n+1,否则必须移动元素才能反映这个逻辑的变化。一般情况下,在第i(1<=i<=n)个元素之前插入一个元素时,需要将第n至第i(共n-i+1个)元素向后移动一个位置。
线性表的删除操作是使长度为n的线性表(a1,...,ai-1,ai,ai+1,an)变成长度为n-1的线性表(a1,...,ai-1,ai+1,...,an),数据元素ai-1、ai和ai+1之间的逻辑关系发生了变化,为了在存储结构上反映这个变化,同样需要移动元素。一般情况下,删除第i(1<=i<=n)个元素时需将从第i+1至第n(共n-i个)元素依次向前移动一个位置。
图2反映的就是线性表元素插入和删除元素时,元素的移动情况。
图2 线性表插入元素和删除元素
3.1.3 代码
下面的代码是线性表的C语言简单实现,当然,其中也用到了C++的引用特性,编译环境是xp vc6.0,亲测可运行。^_^
#include <stdio.h>
#include <stdlib.h>
//function status code
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//function type, function return value type, correspond to the function status code
typedef int Status;
//element type of a data element
typedef int ElemType;
//LinearList Sequential implement
#define LIST_INIT_SIZE 100//initial size of sequential list
#define LISTINCREMENT 10//incremental size
typedef struct
{
ElemType *elem;//base address of Sqlist
int length;//current number of elements
int listsize;//current storage, count as sizeof(ElemType)
}SqList;
//initialize the list
Status InitList_Sq(SqList &L)
{
//construct a Sqlist L
L.elem=(ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));
//if allocation falure, exit
if (!L.elem)
{
exit(OVERFLOW);
}
L.length=0;
L.listsize=LIST_INIT_SIZE;
return OK;
}
Status ListInsert_Sq(SqList &L,int i,ElemType e)
{
//insert an new element e before the ith element
//the value of i: 1<=i<=L.length+1
//illegal value of i
if (i<1 || i>L.length+1)
{
return ERROR;
}
//running out of space, allocate more.
if (L.length >= L.listsize)
{
ElemType *newbase=(ElemType *)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof(ElemType));
if (!newbase)
{
exit(OVERFLOW);
}
//update the base address of the Sqlist
L.elem=newbase;
//increase the storage size
L.listsize += LISTINCREMENT;
}
//mark the insert location with q
ElemType *q=&(L.elem[i-1]);
//move the element behind elem[i-1], elem[i-1] included
ElemType *p;
for (p=&(L.elem[L.length-1]);p>=q;--p)
{
*(p+1)=*p;
}
//insert the element e
*q=e;
//increase the length of Sqlist
++L.length;
return OK;
}
Status ListDelete_Sq(SqList &L, int i, ElemType &e)
{
//delete the ith element, and storage it into e as return value
//legal i: 1<=i<=L.length
//illegal i
if (i<1 || i>L.length)
{
return ERROR;
}
//mark the delete location
ElemType *p=&L.elem[i-1];
//return value got
e=*p;
//mark the tail of the Sqlist
ElemType *q=L.elem+L.length-1;
//move the element behind L.elem[i-1], L.elem[i-1] included
for (++p;p<=q;p++)
{
*(p-1)=*p;
}
//decrease the Sqlist length
--L.length;
return OK;
}
Status GetElem(SqList L, int i, ElemType &e)
{
//get the ith element, and storage it into e as return value
//legal i: 1<=i<=L.length
//illegal i
if (i<1 || i>L.length)
{
return ERROR;
}
//return value got
e=L.elem[i-1];
return OK;
}
int main()
{
//declare a Sqlist
SqList sqlist;
//initialize the sqlist
InitList_Sq(sqlist);
//insert element 10-19
int i;
for (i=10;i<20;i++)
{
ListInsert_Sq(sqlist,i-9,i);
}
//print the 1-10th element
printf("The 1-10th element is:\n");
for (i=1;i<=10;i++)
{
ElemType temp;
GetElem(sqlist,i,temp);
printf("%d ",temp);
}
printf("\nPlease input the which element you want to delete(1-10):");
scanf("%d",&i);
//mark the deleted element value
ElemType temp;
ListDelete_Sq(sqlist,i,temp);
printf("%dth element(value:%d) DELETED!\nThe remaining element can be as follows:\n",i,temp);
for (i=1;i<=sqlist.length;i++)
{
ElemType temp;
GetElem(sqlist,i,temp);
printf("%d ",temp);
}
printf("\n");
return 0;
}
3.2 线性表的链式表示和实现
3.2.1 线性表的链式表示介绍
线性表的顺序存储结构的特点是逻辑上相邻的两个元素在物理位置上也相邻,因此,可以随机存取表中任一元素。这样做的坏处在于在对其执行插入和删除操作时需要大量地移动元素。这是为什么引入链式表示的原因。
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这里的存储单元 既可以是连续的,也可以是不连续的 )。这样,为了表示每个数据元素ai和其直接后继元素ai+1之间的逻辑关系,对于数据ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息,也就是其直接后继的存储位置。这两部分信组成数据元素ai的存储映像,称为节点(node),这两部分信息通常称其为数据域和指针域。指针域中存储的信息称作指针或链。n个结点(ai(1<=i<=n)的存储映像)链结成一个链表,即为线性表(a1,a2,...,an)的链式存储结构,又由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表。
用线性链表来表示数据时,数据元素之间的逻辑关系是由结点是中的指针指示的。也就是说,指针为数据元素之间的逻辑关系的映像,则映像上相邻的两个数据元素其存储的物理位置不要求相邻,因此,这种存储结构也成为非顺序映像或者链式映像。通常我们把链表画成用箭头相连接的节点的序列,节点之间的箭头表示链域中的指针。
整个链表的存取必须从头指针开始进行,头指针指示链表中第一个结点(即第一个数据元素的存储映像)的存储位置。同时,由于最后一个数据元素没有直接后继,则线性表中最后一个结点的指针为空(NULL)。下面图3是一个链表以及其物理表示和简易逻辑表示。
图3 链表的逻辑表示和物理表示
从这个图中,可以看出,每个结点的位置信息实际是记录在上一个节点的指针域中的,因此, 要访问ai结点,必须知道ai-1结点,要访问ai-1结点,必须要知道ai-2结点... ...因此,单链表是一种非随机的存储结构,也就是说,单链表是一种顺序存储结构。
3.2.2 线性表的链式实现
(1)头结点
有时,我们在单链表的第一个结点之前附设一个节点,称之为头结点。头结点的数据域可以不存储任何信息,也可以存储诸如线性表长度等之类的附加信息,头结点的指针域存储指向第一个结点的指针(即第一个元素结点的存储位置)。图4中是一个带头结点的单链表。单链表的头指针指向头结点,若线性表为空,则头结点的指针域为空。
图4 带头结点的单链表
(2)插入结点
图5 插入结点
图5中展示了向单链表中插入结点的过程:1)s->next=p->next; 2)p->next=s;这里1和2的顺序必须要注意下,一定要先将带插入结点指向带插入结点位置的下一个结点(因为如果反过来的话,节点b就找不到了)。
(3)删除节点
图6 删除结点
图6展示了删除节点的过程:1)q=p->next; (首先用临时变量q记录下被删除结点的位置)2)p->next=p->next->next; 3)free(q)(释放q结点的空间)。
(4)建立链表
单链表和顺序存储结构不同,它是一种动态结构。整个可用存储空间可为多个链表共同享用,每个链表占用的空间不需要预先分配划定,而是可以由系统应需求即时生成。因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各个元素结点,并逐个插入链表。
(5)链表的合并
这里讨论的是将两个带头结点并且元素非递减排列的链表合并成一个有序链表。应该会用到三个指针,pa、pb和pc,其中pa和pb分别指向当前待比较插入的链表节点,pc指向合并后的结点的最后一个元素。如果pa->data <= pb->data,则将pa指向的节点插入到pc后面,否则则将pb指向的节点插入pc后,然后注意更新pa、pb指向下一个节点。
这里需要注意的地方是在归并两个链表为一个链表时,不需要另建新的表结点空间,而只需将原来的两个链表中结点之间的关系解除,重新按元素值非递减的关系将所有节点连接成一个链表即可。
3.2.3 代码实现
下面简单实现了带头结点的链表的建立、插入、删除、合并和显示(也就是遍历)等操作。
#include <stdio.h>
#include <stdlib.h>
//function status code
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//function type, function return value type, correspond to the function status code
typedef int Status;
//element type of a data element
typedef int ElemType;
//the definition of a link node
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
//create a link list with head node according to the input
void CreateList_L(LinkList &L, int n)
{
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;//create an empty link list with head node
int i;
for (i=n;i>0;--i)
{
LinkList p=(LinkList)malloc(sizeof(LNode));
printf("input the number you wanna storage in the list:");
//get the node data from the input
scanf("%d",&p->data);
//insert the node to the head of the link list
p->next=L->next;
L->next=p;
}
}
//Given a link list L with head node, if ith element exists, assign it to e and return ok
Status GetElem_L(LinkList L,int i,ElemType &e)
{
//p point to the first element
LinkList p=L->next;
int j=1;
while (p&&j<i)
{
p=p->next;
++j;
}
//ith element does not exists or i is not a correct position
if (!p||j>i)
{
return ERROR;
}
e=p->data;
return OK;
}
//Given a link list with head node, insert e before the i position.
Status ListInsert_L(LinkList &L,int i,ElemType e)
{
LinkList p=L;
int j=0;
while (p&&j<i-1)
{
p=p->next;
j++;
}
//i is less than 1 or i is greater than len(list)+1
if (!p||j>i-1)
{
return ERROR;
}
//create a new node
LinkList s=(LinkList)malloc(sizeof(LNode));
s->data=e;
//insert at the i position
s->next=p->next;
p->next=s;
return OK;
}
//Given a link list with head node, delete the ith element and assign it to e
Status ListDelete_L(LinkList &L,int i,ElemType &e)
{
//set p as the direct pioneer of the element to be removed
LinkList p=L;
int j=0;
while(p->next&&j<i-1)
{
p=p->next;
++j;
}
//i is not a correct position
if (!(p->next)||j>i-1)
{
return ERROR;
}
//keep q as a temp pointer for space free
LinkList q=p->next;
//assign the deleted element to e
e=q->data;
//delete the ith element
p->next=p->next->next;
//free ith node space
free(q);
}
void ShowList_L(LinkList L)
{
LinkList p=L;
if (!(p->next))
{
printf("Now p is an empty list.\n");
}
while (p->next)
{
printf("%4d",p->next->data);
p=p->next;
}
printf("\n");
}
//Given two link list with head node(La,Lb), merge it into Lc(with head node)
void MergeList_L(LinkList &La,LinkList &Lb, LinkList &Lc)
{
//La,Lb is in nondecreasing order and so does the new created Lc
LinkList pa = La->next;
LinkList pb = Lb->next;
//using the head node of La
LinkList pc = La;
Lc = pc;
while (pa && pb)
{
if (pa->data <= pb->data)
{
pc->next = pa;
pc = pa;
pa = pa->next;
}else
{
pc->next = pb;
pc = pb;
pb = pb->next;
}
}
//add the left to the tail of Lc
pc->next = pa?pa:pb;
//free the head node of Lb
free(Lb);
}
int main()
{
LinkList L;
int n;
printf("Input the length of the list you wanna create:");
scanf("%d",&n);
CreateList_L(L,n);
ShowList_L(L);
ListInsert_L(L,2,3);
printf("%d has been insert at position %d.\n",3,2);
ShowList_L(L);
int temp;
ListDelete_L(L,3,temp);
printf("%d has been removed from the list.\n",temp);
ShowList_L(L);
//now create anther link list
printf("Now create the other link list.\n");
LinkList LL;
int nn;
printf("Input the length of the list you wanna create:");
scanf("%d",&nn);
CreateList_L(LL,nn);
printf("Link list has been created:\n");
ShowList_L(LL);
printf("Merge the two list above and the result can be seen as below:\n");
LinkList LLL;
MergeList_L(L,LL,LLL);
ShowList_L(LLL);
return 0;
}
上面程序的运行结果如下图7。
图7 链表的插入和删除
ps:这里我尽量避免“节点”和“结点”的乱用,但是,好像还是乱了好多地方。>.<