数据结构——链表

目录

文章目录

前言

链表种类

一.单链表

1.头指针与头节点

 2.不带头结点的单链表的插入操作

3.带头结点的单链表的插入操作

4.删除操作

5.代码展示

二.双向链表

1.优点

2.插入操作

3.删除操作

4.代码演示

三. 循环链表

1.优点

2.合并两个循环链表

3.代码演示

 四.循环双向链表

总结


文章目录

  • 前言
  • 链表种类
    • 1.单链表
    • 2.双向链表
    • 3.循环链表
    • 4.循环双向链表
  • 总结

前言

刚才我们了解到,数组作为数据存储结构有一定的缺点。在无序数组中,搜索时十分低效。在有序数组中,插入的效率很低。不管在哪种数组中,删除的效率都很低。而且大小无法改变。为了应对顺序表的缺陷,链表就此诞生。链表也是继数组之后第二种使用的最广泛的通用数据结构。

链表结构:在物理上不连续,在逻辑上连续。大小不固定。

链式存储结构是基于指针实现的。我们把一个数据元素和一个指针称之为节点

数据域:存数据元素的区域
指针域:存储直接后继位置的区域。
链式存储,其实就是用指针将相互关联的结点链接起来。


链表种类

一.单链表

概念:链表的每个节点只包含一个指针域。叫做单链表(即构成链表的每个节点只有一个指向后继节点的指针)

1.头指针与头节点

单链表有带头节点和不带头结点的两种结构。

链表中,第一个结点存储的位置叫头指针,如果链表有头结点,那么头指针就是指向头结点的指针。
头指针所指的不存在数据元素的第一个结点就叫做头结点(而头结点又指向首元结点)。
头结点一般不放数据(有的时候也是放的,比如链表的长度,用做监视)。
存放第一个数据元素的结点叫做第一个数据元素结点,也叫做首元结点。

 

 2.不带头结点的单链表的插入操作

上图中,是不带头结点的单链表的插⼊操作。如果我们在非第一个结点之前做插入操作,我们只需要把新结点的指针域指向a(i),然后将a(i - 1)的指针域指向新的结点。如果我们在第一个结点之前进行插入操作,那么头指针就要等于新插入的节点,这和在非第一个数据元素结点前插入时的情况不同。而且,还有一些不同的情况需要我们做考虑。
所以,当我们设计此类链表的时候,就要分别设计实现方法。不如带头结点的方便。

3.带头结点的单链表的插入操作

上图中,如果采用带头结点的单链表结构,算法实现的时候,p指向头结点,改变p的指针的next的值,就可以了,而头指针head的值不变。因此,算法的实现方法比较简单,其操作与对其他结点的操作统一。

带头结点的好处就是,方便对于链表的操作。对于空表、非空表的情况以及对于首元结点都可以进行统一的处理。

4.删除操作

我们统一用带头结点的情况

设a(i)是要删除的,要实现将a(i)删除,就是将它的前继节点的指针绕过,指向它的后继节点就ok。

5.代码展示

#include<stdio.h>
#include<stdlib.h>
//声明链表的节点结构
typedef struct ListNode{
    int data;
    struct ListNode* next;
}Node,*linklist;
//linklist与*Node,都是结构体指针,用linklist声明的指针,强调标记了一个单链表,Node*声明的指针,强调标记了一个节点 
//初始化一个带头节点的单链表
linklist initlist()
{
    linklist l=(Node*)malloc(sizeof(Node));
    if(l==NULL) printf("分配失败\n");
    else l->next=NULL;
    return l;
}
//头插法
linklist head_insert(linklist head,int k)
{
    Node* s=(Node*)malloc(sizeof(Node));
    if(s==NULL) printf("分配失败\n");
    else {
        s->data=k;
        s->next=head->next;
        head->next=s;
    }
    return head;
}
//尾插法
linklinst tail_insert(linklist head,int k)
{
    Node* s=(Node*)malloc(sizeof(Node));
    if(s==NULL) printf("分配失败\n");
    else{
        s->data=k;
        s->next=NULL;
        //先找到最后一个节点
        Node* t=head;
        while(t){
            if(t->next==NULL) break;
            t=t->next;
        } 
        t->next=s;
    }
    return head;
}
//在数据x后插入k
linklist mid_insert(linklist head,int x,int k)
{
    Node* s=(Node*)malloc(sizeof(Node));
    if(s==NULL) printf("分配失败\n");
    else{
        s->data=k;
        //找到x在的节点
        Node* t=head->next;
        while(t->data!=x) t=t->next;
        s->next=t->next;
        t->next=s;
    }
    return head;
}
//删除数据k
linklist del(linklist head,int k)
{
    //找到k的位置
    Node* p=head->next;//找到k的节点
    Node* pre=head;//找p的前一个节点
    while(p->data!=k){
        p=p->next;
        pre=pre->next;
    }
    pre->next=p->next;
    p->next=NULL;
    free(p);//释放p指的节点空间
    p->NULL;//释放p节点,防止变成野指针
    return head;
}
//打印链表
void print(linklist head)
{
    Node* p=head->next;
    while(p){
        printf("%d ",p->data);
        p=p->next;
    }
    printf("\n");
}
int main()
{
    //运行结果3 2 7 4 8 5 6
    linklist head=initlist();
    head=head_insert(head,1);
    head=head_insert(head,2);
    head=head_insert(head,3);
    head=tail_insert(head,4);
    head=tail_insert(head,5);
	head=tail_insert(head,6);
	head=mid_insert(head,2,7);
	head=mid_insert(head,4,8);
	del(head,1);
	print(head);
	return 0;
}

二.双向链表

 双向链表在单链表的基础上维护前继指针即可

1.优点

单链表并不能访问前继节点,如果要寻找的话,就需要遍历,最坏的时间复杂度是O(n),此时双向链表的作用就显示出来了

2.插入操作

s->pre=p;//1
s->next=p->next;//2
p->next->pre=s;//3
p->next=s;//4

3.删除操作

p->pre->next=p->next;//1
p->next->pre=p->pre;//2

4.代码演示

#include<stdio.h>
#include<stdlib.h>
//声明链表
typedef struct ListNode {
	int data;
	struct ListNode* next;
	struct ListNode* pre;
}Node,*linklist;

//初始化
linklist initlist()
{
	linklist s = (Node*)malloc(sizeof(Node));
	if (s == NULL) printf("失败\n");
	else {
		s->next = s;
		s->pre = s;
	}
	return s;
}

//头插法
linklist head_insert(linklist head, int k)
{
	Node* s = (Node*)malloc(sizeof(Node));
	if (s == NULL) printf("失败\n");
	else {
		s->data = k;
		s->pre = head;
		s->next = head->next;
		head->next = s;
		if (s->next != NULL)
			s->next->pre = s;
	}
	return head;
}

//尾插法
linklist tail_insert(linklist head,int k)
{
	Node* s = (Node*)malloc(sizeof(Node));
	if (s == NULL) printf("失败\n");
	else {
		s->data = k;
		s->next = NULL;
		Node* t = head;
		while (t != NULL)
		{
			if (t->next == NULL) break;
			t = t->next;
		}
		t->next = s;
		s->pre = t;
	}
	return head;
}

//在数据x后面插入k
linklist mid_insert(linklist head, int x, int k)
{
	Node* s = (Node*)malloc(sizeof(Node));
	if (s == NULL) printf("失败\n");
	else {
		s->data = k;
		Node* t = head->next;
		while (t->data != x && t != NULL)
			t = t->next;
		s->pre = t;
		s->next = t->next;
		t->next = s;
		if (s->next != NULL)
			s->next->pre=s;
	}
	return head;
}

//删除数据k
linklist del(linklist head, int k)
{
	Node* p = head->next;
	Node* pr = head;
	while (p != NULL && p->data != k)
	{
		p = p->next;
		pr = pr ->next;
	}
	pr->next = p->next;
	if (p->next != NULL)
		p->next->pre = pr;
	p->next = p ->pre = NULL;
	free(p);
	p = NULL;
	return head;
}
//打印
void print(linklist head)
{
	Node* p = head->next;
	while (p != NULL)
	{
		printf("%d ", p->data);
		p = p->next;
	}
	printf("\n");
}

//主函数
int main()
{
	linklist head = initlist();
	head = head_insert(head, 1);
	head = head_insert(head, 2);
	head = head_insert(head, 3);
	head = tail_insert(head, 4);
	head = tail_insert(head, 5);
	head = tail_insert(head, 6);
	head = mid_insert(head, 2, 7);
	head = mid_insert(head, 4, 8);
	del(head, 1);

	print(head);

	return 0;
}

三. 循环链表

1.优点

循环列表是一种特殊的链表,它的最后一个节点指向第一个节点,形成了一个环形的结构,方便循环操作和轮流访问,在需要循环遍历链表,轮流访问数据,在链表头尾进行插入和删除的操作场景下非常有用。插入和删除和单链表差不多因此就不单独讨论。

2.合并两个循环链表

 rearA->next和rearB->next都是空的头节点,合并时被合并的那个链表中的头节点被删掉,它们的尾指针分别是rearA和rearB,需要合并他们,只需要如下操作

p=rearA->next;//保存A表的头节点即1
rearA->next=rearB->next->next;//跳过B的头节点
q=rearB->next;//赋值给rearA->next即2
rearB->next=p;//将原来A表的头节点赋值给rearB->next即3

 忘记加上free(q)操作了

3.代码演示

#include<stdio.h>
#include<stdlib.h>
//循环链表->除开初始化操作和单链表不同其他相同
//需要在初始化时,让头节点自己指向自己
typedef struct Nodelist {
	int data;
	struct Nodelist* next;
}node,*linklist;

//初始化带头节点的循环单链表
linklist initlist()
{
	linklist s = (linklist)malloc(sizeof(node));
	if (s == NULL) printf("失败\n");
	else {
		s->next = s;
	}
	return s;
}

 四.循环双向链表

名字可知道是结合了循环链表和双向链表的产物,同样重复操作过多,因此代码展示不同

//循环双向链表
typedef struct NodeList {
	int data;
	struct NodeList* next;
	struct Nodelist* pr;
}node,*linklist;

//初始化
linklist initlist()
{
	linklist s = (linklist)malloc(sizeof(node));
	if (s == NULL) printf("失败\n");
	else {
		s->next = s;
		s->pr = s;
	}
	return s;
}

总结

综上所述,在单链表的基础上演化了众多链表,本质就是处理不同情况下特有的高效结构。处理上述几种还有静态链表等,但是静态链表本质是还是数组操作,有兴趣可以看看以下的静态链表代码:

#include <string.h>   
#include <stdio.h>    
#include <stdlib.h>   
#include <math.h>  
#include <time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 1000 /* 存储空间初始分配量 */
typedef int Status;           /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef char ElemType;        /* ElemType类型根据实际情况而定,这里假设为char */
Status visit(ElemType c)
{
    printf("%c ",c);
    return OK;
}

/* 线性表的静态链表存储结构 */
typedef struct 
{
    ElemType data;
    int cur;  /* 游标(Cursor) ,为0时表示无指向 */
} Component,StaticLinkList[MAXSIZE];


/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space) 
{
	int i;
	for (i=0; i<MAXSIZE-1; i++)  
		space[i].cur = i+1;
	space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
	return OK;
}


/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space) 
{ 
	int i = space[0].cur;           		/* 当前数组第一个元素的cur存的值 */
	                                		/* 就是要返回的第一个备用空闲的下标 */
	if (space[0]. cur)         
	    space[0]. cur = space[i].cur;       /* 由于要拿出一个分量来使用了, */
	                                        /* 所以我们就得把它的下一个 */
	                                        /* 分量用来做备用 */
	return i;
}


/*  将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k) 
{  
    space[k].cur = space[0].cur;    /* 把第一个元素的cur值赋给要删除的分量cur */
    space[0].cur = k;               /* 把要删除的分量下标赋值给第一个元素的cur */
}

/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
    int j=0;
    int i=L[MAXSIZE-1].cur;
    while(i)
    {
        i=L[i].cur;
        j++;
    }
    return j;
}

/*  在L中第i个元素之前插入新的数据元素e   */
Status ListInsert(StaticLinkList L, int i, ElemType e)   
{  
    int j, k, l;   
    k = MAXSIZE - 1;   /* 注意k首先是最后一个元素的下标 */
    if (i < 1 || i > ListLength(L) + 1)   
        return ERROR;   
    j = Malloc_SSL(L);   /* 获得空闲分量的下标 */
    if (j)   
    {   
		L[j].data = e;   /* 将数据赋值给此分量的data */
		for(l = 1; l <= i - 1; l++)   /* 找到第i个元素之前的位置 */
		   k = L[k].cur;           
		L[j].cur = L[k].cur;    /* 把第i个元素之前的cur赋值给新元素的cur */
		L[k].cur = j;           /* 把新元素的下标赋值给第i个元素之前元素的ur */
		return OK;   
    }   
    return ERROR;   
}

/*  删除在L中第i个数据元素   */
Status ListDelete(StaticLinkList L, int i)   
{ 
    int j, k;   
    if (i < 1 || i > ListLength(L))   
        return ERROR;   
    k = MAXSIZE - 1;   
    for (j = 1; j <= i - 1; j++)   
        k = L[k].cur;   
    j = L[k].cur;   
    L[k].cur = L[j].cur;   
    Free_SSL(L, j);   
    return OK;   
} 

Status ListTraverse(StaticLinkList L)
{
    int j=0;
    int i=L[MAXSIZE-1].cur;
    while(i)
    {
            visit(L[i].data);
            i=L[i].cur;
            j++;
    }
    return j;
    printf("\n");
    return OK;
}


int main()
{
    StaticLinkList L;
    Status i;
    i=InitList(L);
    printf("初始化L后:L.length=%d\n",ListLength(L));

    i=ListInsert(L,1,'F');
    i=ListInsert(L,1,'E');
    i=ListInsert(L,1,'D');
    i=ListInsert(L,1,'B');
    i=ListInsert(L,1,'A');

    printf("\n在L的表头依次插入FEDBA后:\nL.data=");
    ListTraverse(L); 

    i=ListInsert(L,3,'C');
    printf("\n在L的“B”与“D”之间插入“C”后:\nL.data=");
    ListTraverse(L); 

    i=ListDelete(L,1);
    printf("\n在L的删除“A”后:\nL.data=");
    ListTraverse(L); 

    printf("\n");

    return 0;
}

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

多喝烧碱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值