一、单链表
单链表,就是线性表的链式存储结构,又称线性链表。
它的特点是:用一组任意的存储单元存储线性表的数据元素,即结点之间在逻辑上是连续的,在物理上是不连续的。
单链表是带有头结点的,头节点只起到“哨兵”的作用,它的数据域是不使用的,指针域指向第一个数据域的位置。
以下代码是单链表各个结点的逻辑表示:data是节点的数据域,next是指针域——存储下一个结点的地址。
typedef struct Node
{
int data;
Node *next;
}Node,*List;
见下图:
二、单链表的相关操作
一共十二种常用操作,头文件声明见下:
//1.初始化
void InitList(List plist);
//2.头插
bool Insert_head(List plist, int key);
//3.尾插
bool Insert_tail(List plist, int key);
//4.查找
Node *Search(List plist, int key);
//5.删除数据结点
bool Delete(List plist, int key);
//6.得到单链表的长度,不包括头结点
int Getlength(List plist);
//7.判断单链表是否为空
bool IsEmpty(List plist);
//8.打印单链表的数据
void Show(List plist);
//9.清空链表
void clear(List plist);
//10.摧毁单链表的数据(释放数据结点)
void Destroy(List plist);
//11.得到pos位置的数据
bool GetElem(List plist, int pos, int key);
//12.逆置
void Reverse(List plist);
//13.取出重复的值
void Unique(List plist);
1. 初始化 void InitList(List plist);
void InitList(List plist)
{
assert(plist!=NULL);
if(plist==NULL)
{
return ;
}
plist->next = NULL;
}
初始化一个单链表,就是把头结点的next置空,即没有一个数据结点。
2.头插
bool Insert_head(List plist, int key);
{
assert(plist!=NULL);
if(plist==NULL)
{
return false;
}
Node *p = (Node*)malloc(sizeof(Node));//利用动态内存 新创建一个节点
p->data = key;//将数据存入新结点的数据域
p->next = plist->next;//先把后面的结点牵住
plist->next = p;//头结点把插入的点牵住
return true;
}
头插法,即在头结点后面插入新的节点,我们需要解决的问题是:如何将新的结点与它的前驱(头结点)、后继联系在一块?
我们不妨换个角度思考,一排的人手拉手,第一个人的后面要插进来一个人,第一个人肯定要先将后面的人交到新人的手里,再把新人牵住,才不会把后面的人丢掉。见下图:
头插法的具体过程:
三、尾插
bool Insert_tail(List plist, int key)//插入的结点变为新的尾结点
{
assert(plist!=NULL);
if(plist==NULL)
{
return false;
}
Node *q;//找到尾结点
Node *p = (Node*)malloc(sizeof(Node));//创建新结点
p->data = key;
for(q=plist; q->next!=NULL; q=q->next);//遍历找到尾结点
p->next = q->next;//
q->next = p;
return true;
}
尾插法,即在当前尾结点的后面插入新的结点,插入的结点变为新的尾结点。具体实现过程见下图。
四、查找
遍历数据结点,找到目标结点,并且返回数据结点中数据域的内容,因此for循环里p从plist->next开始,循环条件为p!=NULL。
Node *Search(List plist, int key)
{
assert(plist!=NULL);
if(plist==NULL)
{
return NULL;
}
Node *p;
for(p=plist->next; p!=NULL; p=p->next)
{
if(p->data == key)
{
return p;
}
}
return NULL;
}
五、删除数据结点
删除数据结点q之前,必须将q->next交给q的前驱,以免丢失节点,所以通过前驱找到删除结点之后,完成交接,再删除结点。链表中的所有前驱包括头结点到尾结点前的一个结点,所以for循环中的p从plist开始遍历,循环条件为p->next!=NULL。
bool Delete(List plist, int key)
{
assert(plist!=NULL);//注意什么时候可能出现空指针,使得程序崩溃
if(plist==NULL)
{
return false;
}
Node *p;//动态查找删除结点
for(p=plist; p->next!=NULL; p=p->next)
{
if(p->next->data==key)//找到删除结点
{
Node *q = p->next;//q为要删除的结点
p->next = q->next;
free(q);
return true;
}
}
return false;
}
六、得到单链表的长度
单链表的长度指的是数据结点的个数,并不包括头结点。遍历所有的数据结点,返回数据结点的个数,for循环里p从plist->next开始,循环条件为p!=NULL。
int Getlength(List plist)
{
int count = 0;
Node *p;
for(p=plist->next; p!=NULL; p=p->next)
{
count++;
}
return count;
七、判断单链表是否为空
单链表的尾结点中,指针域存放的是NULL,所以当链表为空时,头结点就是尾结点,即plist->next==NULL。
bool IsEmpty(List plist)
{
return plist->next==NULL;//看头结点的指针next的指向,是否为空
}
八、打印单链表的数据
遍历所有的数据结点,打印他们的数据域。for循环里p从plist->next开始,循环条件为p!=NULL。
void Show(List plist)
{
Node *p;
for(p=plist->next; p!=NULL; p=p->next)
{
printf("%d ",p->data);
}
printf("\n");
}
九、清空单链表
引用了摧毁方法。
void clear(List plist)
{
Destroy(plist);
}
十、摧毁单链表(释放数据结点)
摧毁单链表,即释放所有的数据结点,见下图。
摧毁单链表一共有两种方法。
方法一:动态删除各个结点
其中,指针q保证p可以一直向后移动,来销毁各个结点。
具体实现代码:
void Destroy(List plist)
{
assert(plist!=NULL);
if(plist==NULL)
{
return ;
}
Node *p = plist->next;//从第一个数据结点开始
Node *q;
while(p!=NULL)
{
q = p->next;//保证结点一直向后移动
free(p);
p = q;
}
plist->next = NULL;//头结点一定要置空,否则二次摧毁的时候,程序会出问题
}
方法二:总是删除第二个结点(第一个数据结点)
其中,p初始化为plist->next,在销毁p之前,先将再下一个结点的位置信息p->next交给plist->next,这时p与结点的联系断开,销毁。然后,p=plist->next,重复上述操作,即总是在删除第一个数据结点。
具体实现代码:
void Destroy2(List plist)
{
Node *p = plist->next;//从第一个结点开始
while(plist->next!=NULL)//plist->next==NULL时,已经释放完了所有的数据结点
{
p = plist->next;
plist->next = p->next;
free(p);
}
plist->next = NULL;//头节点一定要置空,否则二次摧毁的时候,程序会出问题
}
十一、得到pos位置的数据
线性表中pos需要判断非法值,单链表中同样需要排除非法值。
具体实现代码:
bool GetElem(List plist, int pos, int* rtval)//rtval:输出参数,用于返回多个值的情况
{
assert(plist!=NULL);
if(plist==NULL)
{
return false;
}
if(pos<0 || pos>=Getlength(plist))
{
return false;
}
Node *p;
int i=0;
for(p=plist->next; p!=NULL; p=p->next)
{
if(i==pos)//找到目标
{
*rtval = p->data;//将数据输出
return true;
}
i++;
}
return false;
}
十二、逆置
方法一:类似数组,头尾两个指针向中靠拢,交换数据
该方法较容易想到,实现过程中的一个难点就是——如何让尾指针向前移动,这个问题的解决方法就在于找到这个结点的前驱,这就需要遍历这个尾指针前面的所有数据结点。
具体思路见下图:
1.得到结点的前驱
Node *GetPre(List plist, Node *p)
{
for(Node *q=plist; q->next!=NULL; q=q->next)
{
if(q->next == p)
{
return q;
}
}
return NULL;
}
2.逆置
void Reverse(List plist)
{
if(plist==NULL || plist->next==NULL || plist->next->next==NULL)
{
return ;
}
Node *p = plist->next;
Node *q;
int tmp;
for(q=plist; q->next!=NULL; q=q->next);//找到尾结点
for(int i=0; i<Getlength(plist)/2; i++)//交换次数由链表长度决定
{
tmp = p->data;
p->data = q->data;
q->data = tmp;
p = p->next;//头指针向后移动
q = GetPre(plist,q);//尾指针向前移动
}
}
if(plist==NULL || plist->next==NULL || plist->next->next==NULL),防止程序崩溃。
方法二:改变结点之间的指向
该方法较第一种思路上更简明,不用再交换两个结点的数据了,只需要改变结点之间的指向就可以了。见下图:
创建三个指针p、q、s,p和q用来改变指向,s保证三个指针能正确向后移动,当s==NULL时,p和q正在改变尾结点和他的前驱之间的指向,q=s(NULL)后,指向操作结束,再将头结点plist->next置为p之后,逆置完成。
具体实现代码:
void Reverse2(List plist)
{
if(plist==NULL || plist->next==NULL || plist->next->next==NULL)
{
return ;
}
Node *p = plist->next;
Node *q = p->next;
Node *s;
p->next = NULL;
while(q!=NULL)
{
s = q->next;
q->next = p;//反向
p = q;
q = s;
//s = q->next;//如果放在这里,q的指向已经发生了改变,s并不能移到它之后的那个结点
}
plist->next = p;
}
方法三:头插法的思想
头插法不符合常识,就像插队一样让人无法理解,但是在逆置中它却起到了很大的作用。
先将头节点的next置空,相当于重新做一个链表,只不过所有的数据结点不需要再动态申请,都已经存在了。
void Reverse3(List plist)
{
Node *p = plist->next;
Node *q;//不用先赋值
plist->next = NULL;
while(p!=NULL)
{
q = p->next;
p->next = plist->next;
plist->next = p;
p = q;
}
}
十三、去除重复的值
链表的唯一性,即数据不能重复。
void Unique(List plist)
{
if(plist==NULL || plist->next==NULL || plist->next->next==NULL)
{
return ;
}
Node *p;
Node *q;
Node *s;
for(p=plist->next; p!=NULL; p=p->next)
{
for(q=p; q->next!=NULL; q=q->next)//对节点后面的结点进行遍历,试图寻找到重复的结点(利用前驱)
{
if(q->next->data == p->data)//找到重复的结点
{
s = q->next;//要删除的结点
q->next = s->next;
free(s);
}
}
}
}
四、测试用例
先创建一个头结点,进行初始化之后,再插入各个数据结点,之后分别验证这些方法。
int main()
{
Node head;//创建头结点
InitList(&head);
/*for(int i=0; i<10; i++)
{
Insert_head(&head, i);//头插法
}
Show(&head);*/
for(int i=0; i<10; i++)
{
Insert_tail(&head, i);//尾插法
}
Show(&head);
printf("\n");
//Node *p = Search(&head, 8);//查找结点
//printf("%d\n",p->data);
//int length = Getlength(&head);//求链表的长度
//printf("%d\n",length);
//Delete(&head, 8);//删除结点
//Show(&head);
/*Destroy2(&head);
Destroy2(&head);*/
int rtval = 2;
if(GetElem(&head,1,&rtval))
{
printf("%d\n",rtval);
}
//Reverse(&head);//逆置
//Reverse2(&head);
/*Reverse3(&head);
Show(&head);*/
Insert_head(&head, 3);
Insert_head(&head, 3);
Show(&head);//链表中有三个重复的结点
Unique(&head);//删除重复值
Show(&head);
return 0;
}