线性表的链式存储结构---链表
数据结构中的每个节点对应于一个存储单元,这种存储单元称为存储结点,简称结点。结点由两部分构成,一是数据域,用于存储数据元素值;二是指针域,用于存放指针,指向前一个或后一个结点。链表按照链接的方式常见的有单链表、循环链表、双链表。接下来主要总结一下它们对插入、删除的操作,本篇都是带头结点的链表。
单链表
指向单链表的第一个结点的指针,用head表示,称为头指针;data表示数据域,next表示指针域;其中当头指针为NULL时,表示链表是一个空表。
图示一个长度为3的单链表(假设有数据2、5、3,对应的地址分别为200、500、300),如下:
定义及初始化
typedef struct Node
{
int data;
struct Node *next;
}Node,*List;
void InitList(List head)
{
assert(head != NULL);
head->next = NULL;
//head->data不用操作
}
因为单链表中指针域的存在,使得对表的插入、删除变得简单,只需修改“链”就可实现,不用再进行移动数据。
1.单链表的头插
bool Insert_Head(List head,int val)
{
Node *p = (Node *)malloc(sizeof(Node));
p->data = val;
p->next = head->next;
head->next = p;
return true;
}
需要注意的是:p->next=head->next;与head->next=p;两条语句顺序不可颠倒,因为有头结点的存在,头插时应该先链接当前结点和后面的结点。
2.单链表的尾插
bool Insert_Tail(List head,int val)
{
Node *p = (Node *)malloc(sizeof(Node));
p->data = val;
Node *q;
for(q=head;q->next!=NULL;q=q->next) ;//遍历整个链表
//将p插入在q的后面
p->next = q->next;//相当于p->next = NULL;
q->next = p;
return true;
}
3.单链表的删除
//查找key的前驱
static Node *SearchPri(List head,int key)
{
for(Node *p=head;p->next!=NULL;p=p->next)
{
if(p->next->data == key)
{
return p;
}
}
return NULL;
}
bool Delete(List head,int key)
{
Node *p = SearchPri(head,key);
if(p == NULL)
{
return false;
}
Node *q = p->next;
p->next = q->next;//将q从链表中剔除
//以上两句等价于p->next = p->next->next;
free(q);
return true;
}
4.单链表的摧毁
void Destroy(List head)
{
Node *p;
while(head->next != NULL)//总是删除第一个数据节点
{
p = head->next;
head->next = p->next;
free(p);
}
}
5.重点:单链表的逆置
方法一:头插思想
void Revers(List head)
{
if(head==NULL ||head->next==NULL ||
head->next->next==NULL)
{
return ;
}
Node *p = head->next;
Node *q;
head->next = NULL;
while(p != NULL)
{
q = p->next;
//将p头插进链表
p->next = head->next;
head->next = p;
p = q;
}
}
应当注意 在头插之前
一定一定要将head->next=NULL;这一句是将链表的头结点和后边的链断开。我刚开始对单链表的逆置思想也是头插,但是没有断头结点,导致算法一直不成功,所以写算法的时候应该仔细,同时也应该掌握链表的灵活性。
方法二:多加入一个指针变量
void Revers(List head)
{
if(head==NULL || head->next==NULL || head->next->next==NULL)
{
return ;
}
Node *p = head->next;
Node *q = p->next;
Node *s;
p->next = NULL;
while(q != NULL)
{
s = q->next;
q->next = p;
p = q;
q = s;
}
head->next = p;
}
循环链表
循环链表与单链表的不同就是链表的最后一个结点的指针域保存的是头结点指针。
图示一个长度为3的循环链表(假设有数据2、5、3;地址分别为200、500、300),如下:
定义及初始化
typedef struct CNode
{
int data;
struct CNode *next;
}CNode,*CList;
void InitList(CList head)
{
assert(head != NULL);
if(head == NULL)
{
return ;
}
head->next = head;//循环链表
}
带头结点的循环链表与单链表比较5知:循环链表中需要遍历链表的算法只要将单链表中判断终止条件不为NULL改为不为head既可,其余类似。
需要留心的是循环链表的删除
CNode *Search(CList head,int key)//List
{
for(CNode *p=head->next;p!=head;p=p->next)
{
if(p->data == key)
{
return p;
}
}
return NULL;
}
bool Delete(CList head,int key)
{
CNode *p = Search(head,key);
CNode *q;
if(p == NULL)
{
return false;
}
if(p->next != head)//p不是最后的节点,删除p后面的点,将后面的数据赋值到p中
{
q = p->next;
p->next = q->next;
p->data = q->data;
free(q);
}
else//p是最后一个点
{
for(q=head;q->next!=p;q=q->next) ;//找到p的前驱,然后删除p
q->next = p->next;
free(p);
}
return true;
}
双向链表
双向链表的操作更灵活,从上面的单链表和循环链表我们能明显的感觉到链表中指针不能后退带来的麻烦,但是,有了双向链表后,每个结点除了一个数据域外,有两个指针,一个指向前驱,一个指向后继,这样因为有了指向前驱的指针后,找前驱就变得相当容易了。
图示一个长度为3(假设有数据为2、5、3,;地址分别为200、500、300)的双向链表,如下:
定义及初始化
typedef struct DNode
{
int data;
struct DNode *next;
struct DNode *prio;
}DNode,*DList;
void InitList(DList head)
{
head->next = NULL;
head->prio = NULL;
}
1.双向链表的头插
bool Insert_Head(DList head,int val)
{
DNode *p = (DNode *)malloc(sizeof(DNode));
p->data = val;
p->next = head->next;//1
head->next = p;//2
p->prio = head;
if(p->next != NULL)//特殊情况,p是第一次插入的节点
{
p->next->prio = p;
}
return true;
}
2.双向链表的尾插
bool Insert_Tail(DList head,int val)
{
DNode *p = (DNode *)malloc(sizeof(DNode));
p->data = val;
DNode *q;
for(q=head;q->next!=NULL;q=q->next) ;
//将p插在q的后面
q->next = p;
p->prio = q;
p->next = NULL;
return true;
}
3.双向链表的删除
DNode *Search(DList head,int key)
{
for(DNode *p=head->next;p!=NULL;p=p->next)
{
if(p->data == key)
{
return p;
}
}
return NULL;
}
bool Delete(DList head,int key)
{
DNode *p = Search(head,key);
if(p == NULL)
{
return false;
}
p->prio->next = p->next;
if(p->next != NULL)//p为尾节点
{
p->next->prio = p->prio;
}
free(p);
return true;
}