介绍链表之前,我们还需要了解一下线性表、数组与链表的关系。
线性表是一种线性结构,它的特点是在数据元素的非空有限集中:
- 存在唯一的一个被称作“第一个”的数据元素
- 存在唯一的一个被称作“最后一个”的数据元素
- 除第一个外,集合中的每个数据元素均只有一个前驱
- 除最后一个外,集合中每个数据元素均只有一个后继
简单来谈就是数据元素“一个接一个的排列”,且由同一种类型的数据元素构成的线性结构就是线性表。而线性表又分为两种,顺序表和链表。
顺序表正如其名,是线性表的顺序存储结构,又被常称为向量(vector)如下图:
数据元素在内存的位置是相邻的,而顺序表的其中一种表达形式便是数组。
线性表的链式存储结构——链表
不同于顺序表,链表在逻辑上是连续的,但在物理上不一定是连续的,而是通过指针完成每个内存空间的连接。
链表在内存空间上不连续,为反映出各元素在线性表中的前后关系,除了存储元素本身的信息外,还需添加一个或多个指针域(如上图的next域)。指针域的值叫指针,又称作链,它用来指示数据元素的存储首址。这两部分信息一起组成一个数据元素的存储映像,称存储映像为结点。上图中,head叫这个链表的头指针,指向链表的第一个结点,头指针是链表的必要元素,无论链表是否为空,头指针都不为空。
当链表的第一个节点的数据域无意义时,则这个结点叫做头节点,根据头节点存在与否,可将链表分为有头链表与无头链表,一般情况下,有头链表的数据域可存放链表的长度。上图为有头链表。
而链表还有另一种分法,将链表分为单向链表(上图)、双向链表与循环链表。
单向链表
单向链表是最简单的链表形式,只能单向遍历,下面我们用C语言来实现无头结点单向链表的基本操作
结构定义
我们将结点定义成一个结构来表示,其中里面的数据类型用ElemType表示
// define element type
typedef int ElemType;
// define struct of linked list
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkedList;
// define Status
typedef enum Status {
ERROR,
SUCCESS
} Status;
初始化
传入初始化接口的参数是链表的头指针(二级指针),事实上形参和实参分配的内存空间并不相同,如果说在这里传入的是一个一级指针,那么当调用这个接口时,会另外申请一个空间做指针,就不是拿原来的实参做操作了。使用二级指针能保证传入接口的是我们要操作的指针。
/**
* @name : Status InitList(LinkList *L);
* @description : initialize an empty linked list with only the head node without value
* @param : L(the head node)
* @return : Status
* @notice : None
*/
Status InitList(LinkedList *L)
{
*L = (LinkedList)malloc(sizeof(LinkedList)); //给第一个结点申请空间
if(!(*L)) //若申请失败返回错误
return ERROR;
(*L)->next = NULL;
return SUCCESS;
}
摧毁链表
/**
- @name : void DestroyList(LinkedList *L)
- @description : destroy a linked list, free all the nodes
- @param : L(the head node)
- @return : None
- @notice : None
*/
void DestroyList(LinkedList *L)
{
LinkedList p;
while(*L){
p = (*L)->next;
free(*L);
*L = p;
}
}
插入
插入方式分为前插法和后插法(下图),后插法更符合平常思维方式
/**
- @name : Status InsertList(LNode *p, LNode *q)
- @description : insert node q after node p
- @param : p, q
- @return : Status
- @notice : None
*/
Status InsertList(LNode *p, LNode *q)
{
if(!p) //如果p结点不存在,返回错误
return ERROR;
q->next = p->next;
p->next = q;
return SUCCESS;
}
删除
这里我们要把即将删除的结点数据储存到指针e所指向的内存空间,需要注意的是,在调用这个接口前,要给指针e赋予一个指向的内存空间,这也是新手们(包括我)经常犯的错误,导致q->data的数据没有内存空间存放而报错。
解决办法有:
- 在传入指针之前,malloc一块内存空间
- 传入ElemType变量的地址
/**
* @name : Status DeleteList(LNode *p, ElemType *e)
* @description : delete the first node after the node p and assign its value to e
* @param : p, e
* @return : Status
* @notice : None
*/
Status DeleteList(LNode *p, ElemType *e)
{
if(!p && !p->next)
return ERROR;
LNode *q;
q = p->next;
*e = q->data; //记得调用前检查e是否有指向一个已知的内存空间
p->next = q->next;
free(q);
return SUCCESS;
}
遍历
遍历就没什么要讲的了,循环一次print一次数据数据
/**
* @name : void TraverseList(LinkedList L, void (*visit)(ElemType e))
* @description : traverse the linked list and call the funtion visit
* @param : L(the head node), visit
* @return : None
* @notice : None
*/
void TraverseList(LinkedList L, void (*visit)(ElemType e))
{
LNode *p;
p = L;
while(p){
visit(p->data);
p = p->next;
}
}
/**
* @name : void visit(ElemType e)
* @description : print e
* @param : e
* @return : None
* @notice : None
*/
void visit(ElemType e)
{
printf("%d ",e);
}
搜索
其实和遍历的方法接近,经过遍历去比较数据中是否有e的值
/**
* @name : Status SearchList(LinkedList L, ElemType e)
* @description : find the first node in the linked list according to e
* @param : L(the head node), e
* @return : Status
* @notice : None
*/
Status SearchList(LinkedList L, ElemType e)
{
LNode *p;
p = L;
while(p){
if(p->data == e)
return SUCCESS;
else
p = p->next;
}
return ERROR;
}
反转
反转的话比较有意思,它是通过定义三个指针——前中后,实现反转。中指针将指针指向前一个结点,前指针表示中指针的前一个结点,后指针原在中指针的下一个结点,用于控制前中指针往后移动。
/**
* @name : Status ReverseList(LinkedList *L)
* @description : reverse the linked list
* @param : L(the head node)
* @return : Status
* @notice : None
*/
Status ReverseList(LinkedList *L)
{
if(!(*L) && !(*L)->next)
return ERROR;
LNode *pre, *cur, *Next; //定义前中后三个指针
pre = *L;
cur = (*L)->next; //先将第一个结点的指针域指向NULL
pre->next = NULL;
while(cur){
Next = cur->next; //后指针后移
cur->next = pre; //将中间的指针指向前一个结点
pre = cur; //前中指针后移
cur = Next;
}
*L = pre; //头指针指向“原尾结点”
return SUCCESS;
}
判断是否为循环链表
单向链表和双向链表的尾结点指针域都指向了NULL,而循环链表的尾结点指针域却指向了第一个结点,如果对循环链表遍历的话如果没有限制次数,遍历不会停止。
判断一个链表是否为循环链表也很简单,可以运用初高中“环形操场追及问题”的方法来解决,定义一个快指针和一个慢指针,通过循环,让快指针每次走两个结点,慢指针每次走一个结点,如果是循环链表,那总有一个时间会让两个指针出现在相同的位置(除开始点外的第一次相等)
/**
* @name : Status IsLoopList(LinkedList L)
* @description : judge whether the linked list is looped
* @param : L(the head node)
* @return : Status
* @notice : None
*/
Status IsLoopList(LinkedList L)
{
LNode *fast, *slow;
fast = slow = L; //快慢指针在相同起点
while(fast && fast->next){
slow = slow->next; //slow走一个结点
fast = fast->next->next; //fast走两个结点
if(slow == fast)
return SUCCESS;
}
return ERROR;
}
偶数结点反转
eg:1->2->3->4->5 变成 2->1->4->3->5
类似的反转就叫偶数结点反转,解决这个问题关键在分两类讨论:结点个数为奇数、结点个数为偶数。
/**
* @name : LNode* ReverseEvenList(LinkedList *L)
* @description : reverse the nodes which value is an even number in the linked list, input: 1 -> 2 -> 3 -> 4 output: 2 -> 1 -> 4 -> 3
* @param : L(the head node)
* @return : LNode(the new head node)
* @notice : choose to finish
*/
LNode* ReverseEvenList(LinkedList *L)
{
LNode *pre, *cur, *Next; //定义前中后指针,用途和反转相似
pre = *L;
*L = pre->next;
while(pre && pre->next){
cur = pre->next;
Next = cur->next;
if(cur->next && cur->next->next){ //如果cur->next不存在,结点个数为偶数
pre->next = cur->next->next; //如果cur->next->next不存在,结点个数为奇数
}else{ //如果都存在,遍历未结束
pre->next = cur->next;
}
cur->next = pre; //将偶数结点反转
pre = Next;
}
return *L;
}
搜索中间结点
搜索中间结点就是遍历两次,第一次计算结点个数,找出第几个结点是中间结点,第二次就是抓出中间结点的位置了
/**
* @name : LNode* FindMidNode(LinkedList *L)
* @description : find the middle node in the linked list
* @param : L(the head node)
* @return : LNode
* @notice : choose to finish
*/
LNode* FindMidNode(LinkedList *L)
{
LNode *mid = *L;
int cnt;
for(cnt=0; mid; cnt++)
mid = mid->next;
mid = *L;
for(int i=0; i<cnt/2; i++)
mid = mid->next;
return mid;
}
双向链表
双向链表区别于单向链表的地方在于一个结点有两个指针域,一个指向后节点,一个指向前结点,优势也比较明显,比如要计算链表中最后一次出现数字1的位置,单向链表需要先遍历一次找到第X个结点出现最后一次数字1,再遍历找到该结点位置,而双向链表可以直接从末尾开始遍历,可以大大节省搜索时间,然而缺点就是比单向链表占内存。
结构定义
和单向链表没什么差别,只是在结构中多出来一个指向前结点的指针
// define element type
typedef int ElemType;
// define struct of linked list
typedef struct DuLNode {
ElemType data;
struct DuLNode *prior, *next;
} DuLNode, *DuLinkedList;
// define status
typedef enum Status {
ERROR,
SUCCESS,
} Status;
双向链表的基本操作与单向链表相差不大,这里我就将代码一起发上来。
基本操作
/**
* @name : Status InitList_DuL(DuLinkedList *L)
* @description : initialize an empty linked list with only the head node
* @param : L(the head node)
* @return : Status
* @notice : None
*/
Status InitList_DuL(DuLinkedList *L)
{
*L = (DuLinkedList)malloc(sizeof(DuLinkedList));
if(!(*L))
return ERROR;
(*L)->next = NULL;
(*L)->prior = NULL;
return SUCCESS;
}
/**
* @name : void DestroyList_DuL(DuLinkedList *L)
* @description : destroy a linked list
* @param : L(the head node)
* @return : status
* @notice : None
*/
void DestroyList_DuL(DuLinkedList *L)
{
DuLinkedList p;
while(*L){
p = (*L)->next;
free(*L);
*L = p;
if(p)
p->prior = NULL;
}
}
/**
* @name : Status InsertBeforeList_DuL(DuLNode *p, LNode *q)
* @description : insert node q before node p
* @param : p, q
* @return : status
* @notice : None
*/
Status InsertBeforeList_DuL(DuLNode *p, DuLNode *q)
{
if(!p)
return ERROR;
if(p->prior){
p->prior->next = q;
q->prior = p->prior;
}
q->next = p;
p->prior = q;
return SUCCESS;
}
/**
* @name : Status InsertAfterList_DuL(DuLNode *p, DuLNode *q)
* @description : insert node q after node p
* @param : p, q
* @return : status
* @notice : None
*/
Status InsertAfterList_DuL(DuLNode *p, DuLNode *q)
{
if(!p)
return ERROR;
if(p->next){
p->next->prior = q;
q->next = p->next;
}
q->prior = p;
p->next = q;
return SUCCESS;
}
/**
* @name : Status DeleteList_DuL(DuLNode *p, ElemType *e)
* @description : delete the first node after the node p and assign its value to e
* @param : p, e
* @return : status
* @notice : None
*/
Status DeleteList_DuL(DuLNode *p, ElemType *e) {
if(!p && !p->next)
return ERROR;
DuLNode *q;
q = p->next;
*e = q->data;
q->next->prior = p;
p->next = q->next;
free(q);
return SUCCESS;
}
/**
* @name : void TraverseList_DuL(DuLinkedList L, void (*visit)(ElemType e))
* @description : traverse the linked list and call the funtion visit
* @param : L(the head node), visit
* @return : Status
* @notice : None
*/
void TraverseList_DuL(DuLinkedList L, void (*visit)(ElemType e)) {
DuLNode *p;
p = L;
while(p){
(*visit)(p->data);
p = p->next;
}
}
/**
* @name : void visit(ElemType e)
* @description : print e
* @param : e
* @return : None
* @notice : None
*/
void visit(ElemType e)
{
printf("%d ",e);
}
双向循环链表只是第一个节点的prior指针指向最后一个结点,最后一个结点的next指针指向第一个结点,其它与上述差别不大。这里就不展开叙述了。
参考:
- 算法与数据结构(C语言版) 主编·邓玉洁
- 数据结构:链表https://blog.youkuaiyun.com/juanqinyang/article/details/51351619