单链表C语言

顺序表缺点

顺序表随机访问很方便,但是也会有不足啊:
1、挪动数据时间开销较大:头部/中间的插入删除,需要挪动后面的所有数据,时间复杂度为O(N)
2、增容有代价:增容需要重新申请空间,拷贝数据,释放旧空间,系统消耗不小
3、空间浪费:增容一般增至原来的2倍大空间,会有空间浪费,假如当前容量为100,满了以后增容到200,再继续插入了5个数据,后面没有插入数据了,这就浪费了95个空间
不存在扩容代价、不存在空间浪费、按需申请空间、头部或者中间插入数据而不需要挪动数据的链表可以解决以上问题

链表的概念和结构

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
在这里插入图片描述
注意:
1、链式结构在逻辑上连续,物理结构上不一定连续。(逻辑结构是想象的,物理结构是内存中实际存储的)
2、结点一般从堆上申请
3、堆上申请的空间时按照一定策略分配的,两次申请的空间可能连续也可能不连续
链表的分类
有8种链表结构
(1)单向和双向
在这里插入图片描述
(2)带头单链表和不带头单链表
在这里插入图片描述
(3)非循环链表和循环链表
在这里插入图片描述
以上3种分类的链表一共有222=8种组合,最实用的有两种:
1、无头单向非循环链表:结构简单,一般不会用来单独存储数据。十几种更多是作为其他数据结构的子结构,如哈希桶,图的邻接表等。
2、带头双向循环链表:结构最复杂,一般用来单独存储数据。实际中使用的链表数据结构都是带头双向循环链表。虽然结构复杂,但是使用代码实现以后会发现该结构会带来很多优势,实现反而简单

链表的实现

SList.h 链表函数定义

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

//单向+不带头+非循环
//打印
void print(SLTNode* plist);
//尾插
void SListNodePushBack(SLTNode** plist,SLTDataType x);
//头插
void SListPushFront(SLTNode** plist, SLTDataType x); 
//尾删
void SListPopBack(SLTNode** plist);
//头删
void SListPopFront(SLTNode** plist);
//单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x);
//在pos之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//在pos之前插入x
void SListInsertBefore(SLTNode** plist,SLTNode* pos, SLTDataType x);
//在pos之前插入x,没有头的情况:后插一个值为x的节点,再跟前面的结点值交换。
void SListInsertBeforeNoHead(SLTNode* pos, SLTDataType x);
//删除后一个节点
void SListEraseAfter(SLTNode* pos);
//删除当前位置
void SListEraseCur(SLTNode** plist, SLTNode* pos);

SList.c 链表函数实现

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
 
#include "SList.h"
 
//打印
void SListPrint(SLTNode* plist)
{
	SLTNode* cur = plist;
 
	//找空节点,找到后不再打印
	while (cur != NULL)
	{
		printf("%d->", cur->data);
 
		//让cur指向下一个节点
		cur = cur->next;
	}
	printf("NULL\n");
}
 
//创建新节点
SLTNode* BuySLTNode(SLTDataType x)
{
	//申请空间
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	if (node == NULL)
	{
		return NULL;
	} 
	//数据赋值
	node->data = x; 
	//指针赋值
	node->next = NULL; 
	return node;
}

1、尾插:
在这里插入图片描述


void SListPushBack(SLTNode** pplist, SLTDataType x)
{	
	SLTNode* newNode = BuySLTNode(x);
	//不能使用tail->next != NULL直接作为判断条件,因为有可能是空链表,对空指针解引用会造成非法访问,因此要分两种情况
 
	if (*pplist == NULL)//链表为空
	{
		*pplist = newNode;
	}
	else
	{
		SLTNode* tail = *pplist;
		while(tail->next!=NULL)
		{
		tail=tail->next;
		}
		tail->next=newNode;
	}
}

2、头插,不需要区分空链表和非空链表
在这里插入图片描述


void SListPushFront(SLTNode** pplist, SLTDataType x)
{
	SLTNode* newNode = BuySLTNode(x);
	newNode->next=*pplist;
	*pplist=newNode;
}

3、尾删
在这里插入图片描述

void SListPopBack(SLTNode** pplist)
{
	if(*pplist==NULL)
	{
		return;
	}
	else if((*pplist->next)==NULL)
	{
		free(*pplist);
		*pplist=NULL;
	}
	else
	{
		SLTNode* pre=NULL;
		SLTNode* tail = *pplist;
		while(tail->next!=NULL)
		{
			pre=tail;
			tail=tail->next;
		}
		free(tail);
		tail=NULL;
		pre->next=NULL;
	}
}

4、头删
在这里插入图片描述

void SListPopFront(SLTNode** pplist)
{
	if(*pplist==NULL)
	{
		return;
	}
	else
	{
		SLTNode* new = (*pplist)->next;
		free(*pplist);
		*pplist=NULL;
	}
}

5、单链表查找
在这里插入图片描述

SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
	SLTNode *cur=*pplist;
	while(cur)
	{
		if(cur->data==x)
		{
			return cur;
		}
		cur=cur->next;
	}
	return NULL;
}

6、在pos之后插入
在这里插入图片描述

void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	SLTNode* newNode = BuySLTNode(x);//插入需要先创建结点
	newNode->next=pos->next;
	pos->next=newNode;
}

7、在pos之前插入x
在这里插入图片描述

void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
	SLTNode* newNode = BuySLTNode(x);
	if(pos==*pplist)
	{
		newNode->next=pos;
		*pplist=newNode;
	}
	else
	{
		SLTNode* prev = NULL;//用来记录pos的前一个结点
		SLTNode* cur = *pplist;
		while(cur!=pos)
		{
			prev=cur;
			cur=cur->next;
		}
		newNode->next=cur;
		prev->next=newNode;
	}
}

8、在pos之前插入x,没有头(plist):后插一个值为x的节点,再跟前面的结点值交换
在这里插入图片描述

void SListInsertBeforeNoHead(SLTNode* pos, SLTDataType x)
{
	SLTNode* newNode = BuySLTNode(x);
 
	//后插新节点
	newNode->next = pos->next;
	pos->next = newNode;
 
	//将新结点的值和pos的值进行交换
	SLTDataType temp = pos->data;
	newNode->data = temp;
	pos->data = x;
}

9、删除后一个结点
在这里插入图片描述

//删除后一个节点
void SListEraseAfter(SLTNode* pos)
{
	assert(pos);
 
	if (pos->next == NULL)
	{
		return;
	}
	SLTNode* next = pos->next;
	pos->next = next->next;//pos的下一个指向pos的下一个结点的下一个结点
	free(next);
	next = NULL;
}

10、删除当前位置
在这里插入图片描述

//删除当前位置
void SListEraseCur(SLTNode** pplist, SLTNode* pos)
{
	assert(pos);
	SLTNode* prev = NULL;
	SLTNode* cur = *pplist;
	while (cur != pos)//寻找pos
	{
		prev = cur;
		cur = cur->next;
	}
	prev->next = cur->next;//前一个结点的下一个指向当前结点即pos的下一个
	free(cur);
	cur = NULL;
}

链表应用OJ题

1.删除链表中等于给定值val的所有节点
分析:

(1)要删除某一结点,需要保存该结点的前一个结点(删除当前节点后,前一结点应指向当前结点的下一结点),同时需要保存当前结点的下一结点(删除当前结点后,能够继续向后访问该链表)

(2)操作:

    遍历链表,如果当前结点值==val,就保存当前结点的下一个结点,前一结点指向当前结点的下一结点,释放当前结点,当前结点指向下一结点;

    如果当前结点值!=val,前一结点挪到当前结点位置,当前结点挪到下一结点位置。需要考虑特殊情况,第一个结点值 == val时,此时prev=NULL需要特殊处理。

不带哨兵位头结点解法:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val){
 
    struct ListNode* prev = NULL;
    struct ListNode* cur = head;
 
    while(cur)
    {
        if(cur->val == val)//找到了
        {
            struct ListNode* next = cur->next;//要free(cur),就必须要先保存cur的下一个结点,否则无法继续访问下一结点
            if(prev == NULL)//cur是头,要删除的结点在第一个位置
            {
                free(cur);
                head = next;
                cur = next;
            }
            else
            {
                prev->next = next;
                free(cur);
                cur = next;
            }   
        }
        else
        {
            prev = cur;
            cur = cur->next;    
        }
      
    }   
    return head;
}

带哨兵位头结点解法:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* guardHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    guardHead->next = head;
 
    struct ListNode* prev = guardHead;
    struct ListNode* cur = head;
 
    while (cur)
    {
        if (cur->val == val)
        {
            struct ListNode* next = cur->next;
            prev->next = next;
            free(cur);
            cur = next;
        }
        else
        {
            prev = cur;
            cur = cur->next;
        }
    }
 
    head = guardHead->next;
    free(guardHead);
    guardHead = NULL;
    return head;
 
}

2.反转一个单链表
分析:

解法一:如果只定义两个节点,当n2指向n1时,n2的下一个结点就找不到了。因此要定义3个节点,n1和n2用于翻转,n3用于迭代:n1指向NULL,n2指向n1,n3用于记录n2的下一个结点
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head)
{
    if(head == NULL || head->next == NULL)
    {
        return head;
    }
    
    struct ListNode* n1 = NULL,*n2 = head,*n3 = head->next;
    while(n2)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3)
        {
            n3 = n3->next;
        }
    }
    return n1;
}

解法二:头插法,依次取原链表中的节点头插到新节点
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* cur = head;
    struct ListNode* newHead = NULL;
 
    while(cur)
    {
        struct ListNode* next = cur->next;
 
        cur->next = newHead;
        newHead = cur;
        cur = next;
    }
    return newHead;
}

3.链表的中间结点
分析:只能遍历一遍,可以用快慢指针,慢指针每次走一步,快指针每次走两步
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* slow = head;
    struct ListNode* fast = head;
 
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
 
    return slow;
}

4.输出链表倒数第k个结点
在这里插入图片描述

struct ListNode 
{
	int val;
	struct ListNode *next;
};
 
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) 
{
    
    if(pListHead == NULL)
    {
        return NULL;
    }
 
    struct ListNode* slow = pListHead;
    struct ListNode* fast = pListHead;
    
    while(k--)
    {
        if(fast == NULL)
        {
            return NULL;
        }
        fast = fast->next;
    }
    
    while(fast)
    {
        slow = slow->next;
        fast = fast->next;
    }
    
    return slow;
}

5.合并两个有序链表
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

struct ListNode {
    int val;
    struct ListNode *next;
};
 
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
 
    struct ListNode* head,*tail =  NULL;
    if(l1 == NULL)
    {
        return l2;
    }
    if(l2 == NULL)
    {
        return l1;
    }
 
    if(l1->val < l2->val)
    {
        head = tail = l1;
        l1 = l1->next; 
    }
    else
    {
        head = tail = l2;
        l2 = l2->next; 
    }
 
    while(l1 && l2)
    {
        if(l1->val < l2->val)
        {
            tail->next = l1; 
            l1 = l1->next;
            tail = tail->next; 
        }
        else
        {
            tail->next = l2;
            l2 = l2->next;
            tail = tail->next;
        }
    }
 
    if(l1 == NULL)
    {
        tail->next = l2;
    }
    else
    {
        tail->next = l1;
    }
 
    return head;
 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) C语言下的单链表,可以增加,删除,查找,销毁节点。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值