本文基于严蔚敏《数据结构C语言版》所总结的笔记
2.3线性表得链式表示和实现
线性表得顺序存储结构得缺点是在做插入和删除操作时,需要移动大量数据元素。链式存储结构解决了这个弱点,但是也失去了可随机存取得优点。
2.3.1线性链表
特点:用一组任意的存储单元存储线性表的数据元素(存储单元连续与否并不重要)
数据元素ai与指示ai直接后继的信息(即直接后继的存储位置)这两部分共同组成数据元素ai的存储映像,称为结点。它包括两个域:1)存储数据元素信息的数据域。
2)存储直接后继存储位置的指针域。
指针域种存储的信息称作指针或链。又由于链表的每个结点中只包含一个指针域,故又称为线性链表或单链表。
注:链表的存取必须从头指针开始进行,头指针指示链表中第一个结点的存储位置,同时由于最后一个数据元素没有直接后继,则线性链表最后一个结点的指针域为”空“(NULL)。
有时我们在单链表的第一个结点之前附设一个结点,称之为头结点。头结点的数据域可以不存储信息或者存储表长等附加信息,指针域指向第一个结点的存储位置,如图2.6。头结点的意义是方便链表操作
// ----------------线性表的单链表存储结构定义 ---------------------------
typedef struct LNode {
ElemType data; // 数据域
struct LNode* next; // 指针域,指向后继节点的存储位置
} Lnode, * LinkList;
如图2.7,假设L是LNode型(LinkList型)的变量,则L为单链表的头指针,他指向表中第一个结点,若L为”空“(L=NULL),则缩编是的线性表为空表,长度为零。
算法2.8:单链表找到第i个元素,赋值给e,并返回OK
在线性表的顺序存储结构中,由于逻辑上相邻的两个元素在物理位置上紧邻免责每个元素的存储位置都可以从线性表的起始位置计算得到。二单链表中,任何两个元素之间没有固定的联系。但是每个元素的存储位置都包含在直接前驱结点的信息之中。因此,单链表中取得第i个数据元素必须从头指针出发寻找,因此,单链表是非随机存取的存储结构
//---------------------返回单链表中第i个结点的值 ------------------------
Status GetElem(LinkList L, int i, ElemType& e)
{
Lnode *P; //创建一个新的结点P
P = L->next; //初始化,P指向第一个结点
int j = 1;//设置一个计数器J
while (P&&j<i) //超出范围不执行
{
P = P->next; //顺指针向后查找,知道P指向第i个元素或P为空
j++;
}
if (!P || j > i) //如果超出范围报错
{
cout << "取值位置不合理,无法返回" << endl;
return 0;
}
e = P->data; //取第i个元素赋值给e
return OK;
}
该算法的时间复杂度是O(n),因为while执行次数与被查元素在表中位置有关,如果1<=i<=n,频率为i-1,否则频率为n。
算法2.9:插入
插入 :
为了插入数据x,首先要生成一个数据域为x的结点p,然后插入在单链表中。同时要修改节点a的指针域,令其指向结点x,二结点x的指针域指向结点b,从而实现三个元素的逻辑关系变化。
//---------------在单链表i位置插入元素e ---------------------------
Status ListInsert(LinkList& L, int i, ElemType e)
{//在第i个位置插入,就是第i-1个结点
Lnode* P;//定义新的指针P
Lnode* S;//定义新的指针S
P = L;//结点P等于头结点
int j = 0;//初始化计数器j
while (P && j < i - 1)//不超过范围从头出发寻找第i-1个结点
{
P = P->next;
++j;
}
if (!P || j > i-1)//如果超出范围报错
{
cout << "取值位置不合理,插入失败" << endl;
return ERROR;
}
S = (LinkList)malloc(sizeof(LNode));//生成新的结点S
if (NULL == S)
{
printf("新结点的动态内存分配失败!\n");
exit(-1);
}
S->data = e;//e赋值给结点s的数据域
S->next = P->next;//新结点的指针域指向第i个结点的存储位置
P->next = S;//第i-2个结点的指针域指向插入的第i个结点的存储位置
cout << "插入成功" << endl;
return OK;
}
算法2.10:删除并由e返回值
删除:
删除元素b,仅需要修改结点a中的指针域即可。
//---------删除单链表中位于i位置的结点,成功输出"已删除第i个元素"----------------
Status ListDelete(LinkList& L, int i, ElemType& e)
{
Lnode* P;//新建一个P指针
Lnode* Q;//新建一个Q指针临时保存被删结点地址
P = L;//P指针指向头指针
int j = 0;//新建一个计数器,用来记录读到了第几个结点
while(P->next&&j<i-1)//如果P存在数据域,并且计数器不大于i-1
{
P = P->next;//不断后移P指针
++j;
}
if (!(P->next || j > i - 1))//越界,删除位置不合理
{
return ERROR;
}
Q = P -> next;//用指针Q临时保存待删除指针域的地址
P->next = Q->next;//修改指针域
e = Q->data;//赋值
cout << "删除成功" << endl;
return OK;
}
算法2.11:头插法
顾名思义,头插法是从表头到表尾建立单链表的算法,其时间复杂度为0(n)
//--------------用尾插法构造单链表,新结点插入在单链表尾结点之后--------------------
//用户依次输入n个数据元素
LinkList CreateListTail(LinkList& L, int n)
{
//传入依次为单链表L,单链表长度
L = (LinkList)malloc(sizeof(LNode));//先建立一个带头结点的单链表L
L->next = NULL;//头结点指针域为空
Lnode* P; //新建指针P用来指向新生成的结点
for (int i=0;i<n;++i)
{
P = (LinkList)malloc(sizeof(LNode)); //生成新结点
cout << "请输入第" << i + 1 << "个结点的值" << endl;
cin>>P->data; //输入新结点数据域
P->next = L->next; //P的数据域指向P指针
L->next = P; //插入到表头
}
cout << "创建成功" << endl;
return L;
}
2.3.2循环链表
特点:表中最后一个结点的指针域指向头结点,整个链表形成一个环。从表中任一结点出发均可以找到其它结点
循环链表操作和线性表基本一致,区别在于算反循环甜椒不是P或P->next为空,而是在于他们是否等于头指针。 但可以为了简化操作在循环链表设置尾指针而非头指针,这样在合并两个链表时,仅需将一个表的表尾和另一个表的表头相接。
2.3.3双向链表
单向链表只能顺指针往后寻找其他结点,若要寻找直接前驱,则需要从表头指针出发。为克服这种单向性缺点,可以利用双向链表。顾名思义,双向链表的结点中有两个指针域,其中一个指向直接后继,另一个指向直接前驱。