数据结构与算法 – 表 – 线性表的链式表示
文章目录
线性表的简单操作
声明
typedef struct LNode {
ElemType data;
struct LNode* next;
}Lnode, *LinkList;
// 注意 LNode 为变量名, LinkList 为指针
Status InitList_L(LinkList &L) {
//建立头结点, 其 next 值为空
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
return OK;
}
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
在这里我们定义了一个节点结构, 结构由两部分组成, 数据域和指针域, 其中, 数据域用来保存数据, 指针域用来指向下一个节点在这里我们学习的是带有头结点的指针.
要注意的是, 头结点的数据域通常不保存数据, 也就是说, 头结点的作用就是, 保存第一个节点的位置.
在单链表中, 头结点没有指针指向, 最后一个节点的指针域指向空NULL.
所以在单链表中, 不是所有的节点都有前驱或者后继.
获得第 i 个节点
Status GetElem_L(LinkList L, int i, ElemType& e)
{
//L 为带有头结点的单链表的头指针
// 当第 i 个节点存在的时候, 将值传给 e 返回OK, 否则返回 Error
LinkList p = L->next;
int counter = 1;
while (p && counter < i) { // 当 p 非空且 不到第 i 个节点
p = p->next;
counter++;
}
//在最后一次循环的时候 counter 会加到 counter = i
if (!p || counter > i)
return ERROR;
else
e = p->data;
return OK;
} //GetElem_L
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
这里说一下为什么有第12行的判定 :
如果我们传入的 i 值大于表中已有的节点的数量, 那么在第7行的while循环中, 会因为p指到了表的末尾, 为空而退出循环, 这时候, 我们在 12 行判定 p 为空, 返回ERROR.
如果我们传入的 i 为一个非正整数, 即 小于等于0 的数, 表中不可能存在这个位置, 所以在第7行的判定中不会进入循环, 在12行的时候, counter 大于 i 的值, 也会返回 ERROR.
插入前置节点
Status ListInsert_L(LinkList& L, int i, ElemType& e)
{
// 在线性链表的第 i 个节点之前插入一个节点 e
// 首先我们创造出这个节点, 给它分配好存储空间
LinkList q = (LinkList)malloc(sizeof(LNode));
q->data = e; //数据域赋值
int counter = 0;
LinkList p = L;
while (p && counter < i - 1)
{
p = p->next;
counter++;
} //循环结束之后, counter会等于 i - 1
if (counter > i - 1 || !p)
return ERROR;
q->next = p->next;
p->next = q;
return OK;
}// ListInsert_L
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
第 14 行判定的原因同上.
赋值过程 :
16/17 行不可以颠倒, 如果颠倒了, 则q的指针域会指向自己.
删除第 i 个节点
Status ListDelete(LinkList& L, int i, ElemType& e)
{
// 删除第 i 个节点, 并且以 e 的形式返回
LinkList p = L;
int counter = 0;
while (p && counter < i - 1)
{
p = p->next;
counter++;
}
//循环结束之后, p 应该是 i-1 个位置上的节点
if (!p || counter > i - 1)
return ERROR;
// q 为待删除节点
LinkList q = p->next; //将 p->next 命名为 q
p->next = q->next;
e = q->data; //将q的数据域赋给 e
return OK;
}
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
删除过程:
返回值为 e 的节点的地址
LNode* ElemLocate(LinkList L, ElemType e)
{
//返回 L 中第一个与 e 的值相同的节点的地址
if (!L) return ERROR;
LinkList p = L;
while (p && p->data != e)
p = p->next;
return p;
}
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
如果 没有找到的话, 循环结束之后 p 应该是 NULL,
如果 找到了, 循环结束之后, p->data 应该是 e
所以无论如何, 返回 p 都是对的
线性链表的复杂操作
有序链表的归并
法一
//方法 1 : 将链表 B 归并到 链表 A
void MergeList(LinkList& La, LinkList& Lb)
{
LinkList pa = La->next;
LinkList pb = Lb->next;
LinkList q = La; //pa 和 q 一定是在一条线上并且一前一后的关系
LinkList t; //t 用来临时存储 pb 的前置节点, pa 和 pb 是始终领队的
while(pa && pb) //当pa和pb都没有到表尾
{
if (pa->data <= pb->data) //如果pa不是第一个大于pb的节点
{
q = pa;
pa = pa->next;
} // 使 pa 和 q 向 链表 a 的后面后移动一位
else
{ //pa指向的是第一个大于pb的节点, 这时候将pb指向的节点插入pa与q之间
t = pb; //t 用来临时存储待查入变量
pb = pb->next; //pb 后移, 指向下一个节点
// 将 t 指向的节点插入pa和q之间
t->next = pa;
q->next = t;
/*在将 t 指向的节点插入到了 a 中之后, 这个节点就成了a中的节点,
并且在 pa 的前面, q 的后面, 我们要让 pa 与 q 一前一后, 因此我们把 q 指向了 t */
q = t;
} // 循环结束的条件为 : pa 或 pb 到达了表尾
if (pb)
q->next = pb;
}
}
//代码来源 <<数据结构 习题集>> b站<算法与数据结构`上交版 P4>
归并原理 : 对于遍历过程中 b 中的每一个节点(假设为当前节点pb), 在 a 中从头开始寻找小于b中当前节点的最后一个节点, 插入到其后 , 或者大于当前节点的第一个节点, 插入到其前面.
pa 和 q : 我们在对链表a进行遍历的时候, 一定要有一对指针一前一后(pa 在前, q在后) 指向一对节点, 用来和 当前节点(pb) 进行比较, 如果 pa 大于当前节点 , 另一个 q 小于当前节点, 则当前节点应该插入q 和 pa 指向节点的中间
pb 和 t : pb 指向链表 b 中当前的节点, 而 t 是在插入的过程中, 用来存储 pb 的前置节点的临时变量, pb 与 t 的关系也是一前(pb)一后(t)
归并过程 :
循环结束 :
法二
//方法 2 : 将链表 A B 归并到链表 C
void MergeList(LinkList& La, LinkList& Lb, LinkList& Lc)
{
// 已知单链线性表 La 和 Lb 的节点按照 值 非递减 排列
// 归并 La 和 Lb 得到新的单链线性表 Lc , Lc 的节点也按照 值 非递减排列
LinkList pa = La->next;
LinkList pb = Lb->next;
LinkList pc = Lc = La; //把 La 的头结点作为 Lc 的头结点
//(把 Lb 的头结点作为 Lc 的头结点也可以, 因为都是空指针)
while (pa && pb) //当pb和pa非空
{
if (pa->data <= pb->data)
{
pc->next = pa; //将 pa 指向的节点与c的末尾链接
pc = pa; //pc继续指向c表的末尾
pa = pa->next; //pa 后移
}
else
{
pc->next = pb;
pc = pb;
pb = pb->next;
}
}
pc->next = pa ? pa : pb;
free(Lb);
}
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
归并原理 : 创造出空节点 Lc , pc 指向Lc, Lc为头结点, pc作为中间变量, 总是指向c表的末尾, 我们要做的就是从 a b 表中从小到大 “挑选” 节点, 然后按照顺序插入到 c 表中.pa 和 pb 总是指向 a 表和 b 表的第一个节点.
line 26 : 因为 pc 总是指向c表的末尾, 所以, pc->next == NULL; 所以这句话的意思是 : 判断 pa 是否为空, 空的话就让 pc 的 next 指向 pb , 非空就让其指向 pa.
循环链表
循环链表即 让链表表尾的节点的指针域指向链表的头节点其操作方法与单链表类似, 这里不再详谈.
双向链表
声明
// 双向链表
typedef struct DulLNode {
struct DulLNode* prior; //指向该节点的前驱
ElemType data;
struct DulLNode* next; //指向该节点的后继
}DulLNode, * DuLinkList;
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
在第 i 个节点前面插入
Status LIstInsert_Dul(DuLinkList& L, int i, ElemType& e)
{
//在带有头结点的双向链表的第 i 个的位置之前插入节点 e
int j = 0;
DuLinkList p = L;
while (j < i - 1 && p)
{
p = p->next;
j++;
} //当j == i - 1 的时候, p 指向了表中的第 i - 1 个节点
if (!p) return ERROR; //表的长度小于 1
DuLinkList s = (DuLinkList)malloc(sizeof(ElemType)); // s 为新节点
if (!s) exit(OVERFLOW); //空间不够, 溢出
s->data = e;
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
return OK;
}
//代码来源 <<数据结构>> (清华大学出版社出版, 严蔚敏/吴伟民 编著)
对于图中的① ~ ④(代码的16 ~ 19行) , 不只有一种顺序, 只要保证 ④ p->prior = s;在 ②p->prior->next = s; 的后面即可.不然会导致 s->next 指向 s 本身.