数据结构c语言篇 《二》链表概述,增删改查等多功能实现及相关面试题(上)

Posted 程序猿是小贺

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构c语言篇 《二》链表概述,增删改查等多功能实现及相关面试题(上)相关的知识,希望对你有一定的参考价值。

概述

链表概念

链表,别名链式存储结构或单链表,用于存储逻辑关系为 “一对一” 的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的

链表的结构

链表中每个数据的存储都由以下两部分组成:

数据域指针域

1.数据元素本身,其所在的区域称为数据域;
2.指向直接后继元素的指针,所在的区域称为指针域;

链表实际存储的是一个一个的结点,真正的数据元素包含在这些结点中,如下图。
链表中的结点

头节点和头指针

头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。
头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。
首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。

一个完整链表结构如图

在这里插入图片描述
注意链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。

链表的实现

0515SList.h

// 1、无头+单向+非循环链表增删查改实现
typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
//单链表在pos位置之前插入x
void SListInsertBefore(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
//链表的销毁
void SListDestory(SListNode** pplist);

0515SList.c

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <Windows.h>
#include<assert.h>
#include"0515SList.h"



// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode *node = (SListNode *)malloc(sizeof(SListNode));
	//此处不能使用assert断言,因为node==NULL合法
	if (node == NULL);
	{
		assert(0);
		return NULL;
	}
	node->data = x;
	node->next = NULL;
	return node;
}

// 单链表打印
void SListPrint(SListNode* plist)
{
	SListNode *cur = plist;
	while (cur)
	{
		printf("%d-->", cur->data);
		cur = cur->next;
	}
	printf("NULL\\n");
}

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	//空链表
	if (*pplist == NULL)
	{
		*pplist = BuySListNode(x);
	}
	else// 非空
	{
		//1.先找到链表中的最后一个结点
		//2.插入元素
		SListNode *cur = *pplist;
		while (cur->next)
		{
			//不能使用cur++,因为链表物理地址不连续,则cur++会与cur->next有差异
			cur=cur->next;
		}
		cur->next = BuySListNode(x);
	}
}

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	if (NULL == *pplist)//空链表
	{
		return;
	}
	else if (NULL == (*pplist)->next)// 链表中只有一个结点
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		// 链表中有多个结点
		//1.找到最后一个结点并保存前一个,然后删掉最后一个,最后前一个的next指向NULL
		SListNode *cur = *pplist;
		SListNode *prev = NULL;
		while (cur->next)
		{
			prev = cur;
			cur = cur->next;
		}
		prev->next = NULL;
		free(cur);
	}
}

// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode *newnode = BuySListNode(x);
	newnode->next = *pplist;
	*pplist = newnode;
}

// 单链表头删
void SListPopFront(SListNode** pplist)
{
	SListNode *delNode = NULL;
	assert(pplist);
	if (NULL == *pplist)//空链表
	{
		return;
	}
	else
	{
		 delNode = *pplist;
		*pplist = delNode->next;
		free(delNode);
	}
}

//求链表长度
int Slistsize(SListNode* plist)
{
	int count = 0;
	SListNode *cur = plist;
	while (cur != NULL)
	{
		cur = cur->next;
		count++;
	}
	return count;
}

// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);
	SListNode *cur = plist;
	while (cur)
	{ 
		if (x == cur->data)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	SListNode *newnode = NULL;
	if (pos == NULL)
		return;
	newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
//在之前插入
void SListInsertBefore(SListNode* pos, SLTDateType x)
{
	SListNode *newnode = NULL;
	if (pos == NULL)
		return;
	newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
	SListNode*tmp = pos;
	pos = newnode;
	newnode = tmp;
}

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	SListNode *delNode = NULL;
	if (pos == NULL||pos->next ==NULL)
		return;
	else
	{
		delNode = pos->next;
		pos->next = delNode->next;
		free(delNode);
	}
}

//链表的销毁
void SListDestory(SListNode **pplist)//直接使用头删法
{
	SListNode *delNode = NULL;
	if (NULL == *pplist)//空链表
	{
		return;
	}
	while (NULL != *pplist)//循环删除
	{
		delNode = *pplist;
		*pplist = delNode->next;
		free(delNode);
	}
}

test.c(略)
还有一个.c的实现文件我在这里就不给大家写了,要一一实现的话可能篇幅就有点大了,有机会的话大家自己把代码拷过去自己实现一下,让我偷一小会懒,谢谢~

相关面试题

由于今天工作量有点多,后面的一些题目为了节省时间我就不给大家画图展示了,相互理解一下谢谢大家了,然后大家歇一下这里正菜来了

1.移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
在这里插入图片描述
思路
1.从头开始遍历链表,判断开头是否相同,如果有先删除开头部分。
2.依次向后遍历,进行删除结点的操作。当最后为空时跳出循环,遍历结束。
注意:在删除过程中记得保留该结点的前一个结点

核心代码方法一

struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode*cur=head;
    struct ListNode*prev=NULL;
    while(cur)
    {
       if(cur->val==val)
       {
           if(prev==NULL)  //要移除的元素是第一个
           {
               head=cur->next;
               free(cur);
               cur=head;
           }
           else// 要移除的元素是除第一个以外的任一个
           {
               prev->next=cur->next;
               free(cur);
               cur=prev->next;
           }
       }
       else
       {
           prev=cur;
           cur=cur->next;
       }
    }
    return head;
}

方法二 递归

struct ListNode* removeElements(struct ListNode* head, int val){
    if(head==NULL)
    {
        return NULL;
    }
    head->next=removeElements(head->next,val);
    return head->val==val?head->next:head;
}

2.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
在这里插入图片描述
方法一

1.三指针法
在遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。最后return时候需要注意,cur已经指向NULL,prev才是新的头指针。

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* next= NULL;
    struct ListNode* cur = head;
    struct ListNode* prev = NULL;

    while(NULL != cur){
        next = cur->next;
        cur->next = prev;
        prev = cur;
        cur = next;
    }
    return prev;
}

头插法:将原链表结点逐个拆下来,然后头插到新链表中

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode*cur=head;
    struct ListNode*newhead=NULL;
    if(head==NULL)
    return NULL;
    while(cur)
    {
        head=cur->next;//head指向cur的next,保存下一个的地址
        cur->next=newhead;//让cur的next指向newhead, 
        newhead=cur;//把newhead放到cur的位置
        cur=head;//让cur往后走
    }
    return newhead;
}

3.链表的中间结点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
在这里插入图片描述
思路
这里我们只要使用快慢指针即可
定义两个指针,快指针每次走两步,慢指针每次走一步,同时从头结点开始往后走,若到最后快指针为空或者快指针的next为空循环停止,程序结束,此时要寻找的中间结点就是慢指针所在的结点位置。

核心代码

struct ListNode* middleNode(struct ListNode* head){
    if(head==NULL||head->next==NULL)
    {return head;}
    struct ListNode *fast=head;
    struct ListNode *slow=head;
    while(fast&&fast->next)//fast不为空保证第一步成功,fast->next不为空保证第二步走成功
    {
        fast=fast->next->next;//每次往前走两步
        slow=slow->next;//每次往前走一步
    }
    return slow;
}

当然如果题目要求返回第一个中间结点的话,则只需要定义一个指针用来保存慢指针,然后判断快指针是否为空,若为空则返回该指针。

4.获取该链表中倒数第k个结点。

输入一个链表,输出该链表中倒数第k个结点。
在这里插入图片描述
要求:只能遍历一遍
思路
定义两个指针,一个先走k步然后两个指针再同时走,直到先走的指针为空然后结束,此时第二个指针所在的位置就是倒数第k个结点。

核心代码

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) 
{
    struct ListNode* first=pListHead;
    struct ListNode* second=pListHead;
    //让first先往后走k步
    while(k--)
    {
        if(first == NULL)
            return NULL;
        first = first->next;
    }
    //同时往后走,直到first为空退出
    while(first)
    {
        first = first->next;
        second = second->next;
    }
    return second;
}

5.链表相交

在这里插入图片描述在这里插入图片描述
思路
1.获取两个链表的节点个数。
2.求两个链表的结点个数差。
3.让长的那个链表先走差值步。
4.然后两个指针同时走,相遇的结点既是交点。

核心代码

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode *node1=headA;
    struct ListNode *node2=headB;
    int count1=0;
    int count2=0;
    if(headA==NULL||headB==NULL)//判断两链表是否为空,是的话直接返回NULL
    return NULL;
    while(node1)//遍历链表1,统计节点数
    {
        node1=node1->next;
        count1++;
    }
    while(node2)//遍历链表2,统计节点数
    {
        node2=node2->next;
        count2++;
    }
    int k=count1-count2;
    struct ListNode *nodeA=headA;
    struct ListNode *nodeB=headB;
    if(k>0)//链表一节点数多于链表二,先走
    {
        while(k--)
        {
           nodeA=nodeA->next; 
        }
    }
    else if(k<0)//链表二节点数多于链表一,先走
    {
        int n=0-k;
        while(n--)
        {
            nodeB=nodeB->next;
        }
    }
    else// 两个链表节点数相同,不做处理
    {
     	//不进行任何操作
    }
    while(nodeA)//此时链表1和链表二剩余节点数相同,循环条件哪个都可以,这里也可以使用链表二操作。
    {
        if(nodeA==nodeB)
        {
            break;
        }
        nodeA=nodeA->next;
        nodeB=nodeB->next;
    }
    return nodeA;
}

在这里插入图片描述

以上就是今天的内容,关于那些面试题这里只给出了某一种解答方法,并不代表答案只有一种,由于篇幅原因我在这里就不多赘述了,作者才疏学浅,要是哪里写的有问题欢迎大佬们及时帮忙纠正,共同学习共同进步谢谢!~

未完,待续…

以上是关于数据结构c语言篇 《二》链表概述,增删改查等多功能实现及相关面试题(上)的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 之基础操作及增删改查等

c语言 建立一个链表,实现增删改查

C语言单链表增删改查的实现

C语言单链表增删改查的实现

C语言单链表的基本操作总结(增删改查),建议收藏!

数据结构C语言版 —— 链表增删改查实现(单链表+循环双向链表)