1. 线性表的定义和基本运算
-
线性表的定义: 它是由n个数据元素A1,A2,... ,An组成的有限序列,注意两个词,有限和序列。通常记为 (A1, A2, ... , An)
-
线性表的逻辑特征:
- 有且仅有一个称为开始的元素A1,它没有前趋,仅有一个直接后继A2;
- 有且仅有一个称为终端元素的An,它没有后继,仅有一个直接前趋;
- 其余元素A~i~(2 ≦ i ≦ n-1)称为内部元素,它们都有且仅有一个直接前趋A~i-1~和一个直接后继A~i+1~;
-
线性表常见的运算:
- 置空表InitList(L),构造一个空的线性表L。
- 求表长ListLength(L),返回线性表L中元素个数,即表长。
- 取表中第i个元素GetNode(L,i), 若1 ≦ i ≦ ListLenght(L),则返回第i个元素A~i~。
- 按值查找LocateNode(L,x),在表L中查找第一个值为x的元素,并返回该元素在表L中的位置,若表中没有元素的值为x,则返回0值。
- 插入InsertList(L,i,x),在表的第i个元素之前插入一个值为x的新元素,表L的长度加1。
- 删除DeleteList(L,i),删除表L的第i个元素,表L的长度加1。
2. 线性表的顺序存储和基本运算实现
2.1 线性表的顺序存储
线性表的顺序存储指的是将线性表的数据元素按其逻辑次序依次存入一组地址连续的存储单元里,用这种方法存储的线性表称为顺序表。由于其在存储单元里也是连续的,所以每个元素结点a~i~的存储地址是该元素结点在表中的位置i的线性函数,只要确定了线性表存储的起始位置,线性表中任何一个元素都可以随机存取,所以顺序表是一种随机存取结构。
定义元素结点的代码:
#define ListSize 100 //表空间的大小应根据实际需要来定义,这里假设为100
typedef int DataType; //DataType的类型可根据实际情况而定,这里假设为int
typedef struct
{
DataType data[ListSize]; //数组data用来存放表结点
int lenght; //线性表的当前表长(实际存储元素的个数);
} SeqList;
2.2 顺序表的基本运算实现
在顺序表上,有些运算是很容易实现的。例如,若表L是SeqList类型的顺序表,那么随机存储表中第i个结点,只需要用L.data[i - 1]就可以直接访问;若需要置空或者初始化表,仅需要将当前表长置成0,即L.length=0。因此下面仅列出顺序表的插入和删除两种基本运算的实现。
- 顺序表的插入运算 线性表的插入运算是指在线性表的第i-1个元素和第i个元素之间插入一个新元素x,使长度为n的线性表: ::: hljs-center
(a~1~, a~2~, ... , a~i-1~, a~i~, ... , a~n~)
:::
变为长度为n + 1的线性表: ::: hljs-center
(a~1~, a~2~, ... , a~i-1~, x, a~i~, ... , a~n~)
:::
由于线性表逻辑上相邻的元素在物理结构上也是相邻的,因此在插入一个新元素之后,线性表的逻辑关系发生了变化,其物理存储也要发生相应的变化。除非i等于n+1,否者必须将原线性表的第i,i+1,... ,n这些个元素分别向后移动一个位置,空出第i个位置以便插入新元素x。 插入算法实现如下:
void InsertList(SeqList *L, int i, DataType x)
{
int j;
if (i < 1 || i > L->lenght + 1)
{
printf("position error");
return;
}
if (L->lenght >= ListSize)
{
printf("overflow ");
return;
}
for (j = L->lenght - 1; j >= i - 1; j--)
{
L->data[j + 1] = L->data[j];
}
L->data[i - 1] = x;
L->lenght++;
}
- 顺序表的删除运算 线性表的删除运算指的是将表中第i(1 ≦ i ≦ n)个元素删除,与插入运算相反,插入是向后移动元素,而删除则是向前移动元素,除非i=n时,直接删除终端元素,不需要移动元素。 删除算法实现如下:
DataType DeleteList(SeqList *L, int i)
{
int j;
DataType x;
if (i < 1 || i > L->lenght)
{
printf("position error");
return -1;
}
x = L->data[i];
for (j = i; j < L->lenght; j++)
{
L->data[j - 1] = L->data[j];
}
L->lenght--;
return x;
}
3. 线性表的链式存储结构
线性表顺序存储结构的特点是,在逻辑关系上相邻的两个元素在物理位置上也是相邻的,因此可以随机存储任一元素。但是,当经常需要做插入和删除操作运算时,需要移动大量的元素,运行效率相对较低,而采用链式存储结构就可以避免这些移动。然而,由于链式存储结构存储线性表数据元素的存储空间可能时连续的,也可能是不连续的,因此链表的结点是不可以随机存取。
3.1 单链表(线性链表)
在使用链式存储结构表示每个数据元素a~i~时,除了存储a~i~本身的信息以外,还需要一个存储指示其后继元素a~i+1~存储位置的指针,它包括两个域,存储数据元素的域称为数据域,存储直接后继存储地址的域称为指针域。利用这种存储方式表达的线性表称为链表,由于这种链表的每个结点中只包含一个指针域,因此又称为单链表。
定义链表存储结构:
typedef char DataType;
typedef struct node { //结点类型定义
DataType data; //结点的数据域
struct node * next; //结点的指针域
} ListNode;
typedef ListNode * LinkedList;
ListNode * p; //定义一个指向结点的指针变量
LinkedList head; //定义指向单链表的头指针
LinkedList rear; //定义指向单链表的尾指针
3.2 单链表上的基本运算
1. 建立单链表
动态建立单链表的常用方法有两种,一种时头插法,另一种是尾插法。
- 头插法建表 头插法建表是从一个空表开始,重复读入数据,生成新结点,将读入的数据存放到新节点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。其具体算法如下:
void createListF()
{
char ch;
head = NULL; // 置空单链表
ch = getchar(); // 读入第一个字符
while (ch != '\n') // 读入字符不是结束标志时作循环
{
p = (ListNode *)malloc(sizeof(ListNode)); // 申请新结点
p->data = ch; // 数据域赋值
p->next = head; // 指针域赋值
head = p; // 头指针指向新结点
ch = getchar(); // 读入下一个字符
}
}
- 尾插法建表 头插法建立链表是将新结点插入在表头,算法比较简单,但是新建链表结点的次序和输入时的顺序相反,理解时不太直观。若需要和输入次序一致,则可使用尾插法建立链表。该方法是将新结点插入在当前链表的表尾上,因此需要增设一个尾指针rear,使其始终指向链表中的尾结点。其具体算法如下:
void createListR()
{
char ch;
head = NULL; // 置空单链表
rear = NULL;
ch = getchar(); // 读入第一个字符
while (ch != '\n') // 读入字符不是结束标识符时作循环
{
p = (ListNode *)malloc(sizeof(ListNode)); // 申请新结点
p->data = ch; // 数据域赋值
if (head == NULL)
{
head = p; // 新结点*p插入空表
}
else
{
rear->next = p; // 新结点*p插入到非空表的表尾结点*rear之后
}
rear = p; // 尾指针指向新的表尾结点
ch = getchar(); // 读入下一个字符
}
if (rear != NULL)
{
rear->next = NULL; // 终端结点指针域置空
}
}
为了简化算法,方便操作,可在链表的开始结点之前附加一个结点,并称之为头结点。尾插法建立单链表的算法可以简化如下:
void createListR1()
{
head = (ListNode *)malloc(sizeof(ListNode)); // 申请头结点
head->data = 't';
DataType ch;
rear = head; // 尾指针初始指向头结点
while ((ch = getchar()) != '\n')
{
p = (ListNode *)malloc(sizeof(ListNode)); // 申请新结点
p->data = ch;
rear->next = p; // 新结点链接到尾结点之后
rear = p; // 尾指针指向新结点
}
rear->next = NULL; // 终端结点指针域置空
}
2. 查找运算(带头结点)
在单链表中,任何两个结点的存储位置之间没有固定的联系,每个结点的存储位置包含在其直接前趋的指针域中。因此,在单链表中存取第i个结点时,必须从表头结点开始搜索,所以链表结构是非随机存取的存储结构。
- 按结点序号查找 在单链表中要查找第i个结点,就必须从链表的第1个结点开始(下标为1),下标为0的是头结点。
ListNode *getNodeIndex(LinkedList head, int i)
{
// head为带头结点的单链表的头指针,i为要查找的结点序号
// 若查找成功,则返回查找结点的存储地址(位置),否则返回NULL
ListNode *p;
int j;
p = head->next; // 使p指向第一个结点(开始结点)
j = 1; // j置1
while (p != NULL && j < i)
{
p = p->next;
j++;
}
if (i == j)
{
return p;
}
else
{
return NULL;
}
}
- 按结点值查找 在单链表中按值查找结点,就是从链表的开始结点出发,顺着链逐个将结点的值和给定值K进行比较,若遇到相等的值,则返回该结点的位置,否则返回NULL。具体算法如下:
ListNode *locateNodeK(LinkedList head, DataType k)
{
// head为带头结点的单链表的头指针,k为要查找的结点值
// 若查找成功,则返回查找结点的存储地址(位置), 否则返回NULL
ListNode *pointer = head->next; // p指向开始结点
while (pointer && pointer->data != k) // 循环直到pointer等于NULL或pointer->data等于k为止
{
pointer = pointer->next; // 指针指向下一个结点
}
return pointer; // 若找到值为K的结点,则p指向该结点,否者p为NULL
}
3. 插入运算
与顺序表不同,链表的插入不需要移动结点,但是需要移动指针来进行位置查找。假如需要插入一个数据x到a~i-1`~与a~i~之间,先使p指向a~i-1~的位置,然后生成一个数据域的值为x的新节点*s,再进行插入操作。具体算法如下:
void insertList(LinkedList head, int i, DataType x)
{
// 在以head为头指针的带头结点的单链表中第i个结点的位置上
// 插入一个数据域值为x的新结点
ListNode *pointer, *s;
int j;
pointer = head;
j = 0;
while (pointer != NULL && j < i - 1) // 使pointer指向第i-1个结点
{
pointer = pointer->next;
j++;
}
if (pointer == NULL)
{
printf("the insert position is error!");
return;
}
else
{
s = (ListNode *)malloc(sizeof(ListNode)); // 申请新结点
s->data = x;
s->next = pointer->next;
pointer->next = s;
}
}
4. 删除运算
删除运算就是将链表的第i个结点从表中删去。由于第i个结点的存储地址是存储在第i-1个结点的指针域next中,因此要先使p指向第i-1个结点,然后使得p->next指向第i +1个结点,再将第i个结点释放掉。其具体算法如下:
DataType deleteList(LinkedList head, int i)
{
// 在以head为头指针的带头结点的单链表中删除第i个结点
ListNode *p, *s;
DataType x;
int j;
p = head;
j = 0;
while (p != NULL && j < i - 1) // 使p指向第i-1个结点
{
p = p->next;
j++;
}
if (p == NULL)
{
printf("delete position error!");
exit(0);
}
else
{
s = p->next; // s指向第i个结点
p->next = s->next; // 使p->next指向第i+1个结点
x = s->data; // 保存被删除的值
free(s); // 释放第i个结点的内存
return x; // 返回结点值
}
}
3.3 循环链表
循环链表是链式存储结构的另一种形式。其特点是单链表中最后一个结点(终端结点)的指针域不为空,而是指向链表的头结点,使整个链表构成一个环。因此,从表中任意一个结点开始都可以访问表中其他结点,这种结构形式的链表称为单循环链表。循环链表的结点类型与单链表完全相同,在操作上也与单链表基本一致,差别仅在于算法中循环结束的判断条件不再是p或者p->next是否为空,而是它们是否等于头指针.
3.4 双向链表
单链表和单循环链表的结点中只设有一个指向其直接后继的指针域,因此,从某个结点出发只能顺指针向后访问其他结点。若需要查找结点的直接前趋,则需要从头指针开始查找某节点的直接前趋结点。若希望从表中快速确定一个结点的直接前趋,只要在单链的结点类型中增加一个指向其直接前趋的指针域prior即可。这样形成的链表中有两条不同方向的链,因此称为双向链表。双向链表及其结点类型描述如下:
typedef char DataType;
typedef struct dlnode {
DataType data;
struct dlnode *prior, *next;
} DLNode;
typedef DLNode * DLinkList;
DLinkList head;
为了某些操作运算的方便,双链表也增设一个头结点,若将尾结点和头结点链接起来也就构成了循环链表。
- 插入运算
void DLInsert(DLNode *p, DataType x)
{
//将值为x的新结点插入到到带头结点的双向链表中指定结点*p之前
DLNode *s = (DLNode *)malloc(sizeof(DLNode)); //申请新结点
s->data = x;
s->prior= p->prior;
s->next = p;
p->prior->next = s;
p->prior = s;
}
- 删除运算
DataType DLDelete(DLNode *p)
{
//删除带头结点的双向链表中指定的结点*p
p->prior->next = p->next;
p->next->prior = p->prior;
x = p->data;
free(p);
return x;
}