嗨嗨大家~我们今天继顺序表内容来讲解链表。话不多说,让我们走进本期的学习吧!
目录
一、线性表的链式存储
1 链式存储结构
2 链表的定义
3 链表的分类
二、链表的实现过程
1 链表的打印
2 结点的创建
3 链表的头插
4 链表的头删
5 链表的尾插
6 链表的尾删
7 链表元素的查找
8 在pos位置之前插入一个结点
9 在pos位置之后插入一个结点
10 删除pos位置前的一个结点
11 删除pos位置后的一个结点
三、源代码
四、链表面试题
1 移除链表元素
2 反转链表
3 链表的中间结点
4 链表中倒数第k个结点
5 链表分割
五、链表与顺序表的对比
1 链表的优缺点
1.1 优点
1.2 缺点
2 顺序表的优缺点
2.1 优点
2.2 缺点
一、线性表的链式存储
1 链式存储结构
线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。 因此,为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素a来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。 这两部分信息组成数据元素ai的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点(ai+1(1<=i<=n) 的存储映像)链接成一个链表,又因为此链表的每个结点中仅包含一个指针域,故又称线性链表或单链表。
根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。这里说一下,单链表、循环链表和双向链表用于实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。
2 链表的定义
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑结构是通过链表中的指针链接次序来实现的。整个链表的存取必须从指针开始进行,头指针指示链表中第一个结点(即第一个数据元素的存储映像,也称首元结点)的存储位置。同时,由于最后一个数据元素没有直接后继,则单链表中最后一个结点的指针为空(NULL)。
注:链表的逻辑结构和物理结构是不一致的。
3 链表的分类
链表的结构是多样化的,下面以附图解的形式来介绍8种链表组合结构:
虽然有多种链表结构,但我们在实际中最常用的只有两种结构,如下图:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
二、链表的实现过程
1 链表的打印
- 思路:先定义一个结点指针指向头节点,往后依次遍历,与数组不同的是,不是cur++,而是让cur指向下一个节点,即cur=cur->next。
代码实现:
//打印
void SListPrint(SLTNode* plist)
{
assert(plist); //断言
SLTNode* cur = plist;//让cur指向头结点进行遍历
while (cur != NULL)//注:条件不是cur->next,当cur->next为空便不进入循环,尾元素访问不到
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
附图解:
分析:时间复杂度:O(N);空间复杂度:O(1)。
2 结点的创建
代码实现:
//创建结点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
node->data = x;
node->next = NULL;
return node;
}
3 链表的头插
代码实现:
//头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
//为什么是**pplist?因为我们要改变的是一级指针plist,所以传参进来应该是一个二级指针(传址调用)
{
//如果传入的是空指针也可以正常插入
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
附图解:
分析:时间复杂度:O(1);空间复杂度:O(1)。
4 链表的头删
代码实现:
//头删
void SListPopFront(SLTNode** pplist)
{
//1.没有结点的情况
if (*pplist == NULL)
{
return;
}
//2.多个结点的情况
else
{
//保存一份下一个元素的地址(否则free过后就找不到了)
SLTNode* next = (*pplist)->next; //需要加括号,因为*和->优先级相同
free(*pplist);
*pplist = next;
}
}
附图解:
分析: 时间复杂度:O(1);空间复杂度:O(1)。
5 链表的尾插
代码实现:
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
//创建一个新结点
SLTNode* newnode = BuySLTNode(x);
//如果链表为空,让尾插的结点变成该表的第一个元素
if (*pplist == NULL)
{
*pplist = newnode;
}
else //非空, 直接尾插
{
//先遍历,找尾结点
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
//该新结点定义时就指向NULL了,所以让tail的next指向该结点即可
}
}
附图解:
分析: 时间复杂度:O(N);空间复杂度:O(1)。
6 链表的尾删
代码实现:
//尾删
void SListPopBack(SLTNode** pplist)
{
//1.没有结点的情况
if (*pplist == NULL)
{
return;
}
//2.只有一个结点的情况
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
//3.多个结点的情况
else
{
//前驱指针
SLTNode* prev = NULL;
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
//找尾结点的前驱结点
prev = tail;
//找尾结点
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
附图解:
分析: 时间复杂度:O(N);空间复杂度:O(1)。
7 链表元素的查找
代码实现:
//查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist; //遍历查找
//while (cur != NULL)
while(cur)
{
if (cur->data == x)
return cur; //返回结点指针
else
cur = cur->next;
}
return NULL; //没找到,返回NULL
}
分析: 时间复杂度:O(N);空间复杂度:O(1)。
8 在pos位置之前插入一个结点
代码实现:
//单链表在pos位置之前插入 (麻烦,不建议使用)
void SListInsert(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
//在第一个位置插入 => 相当于头插
if (pos == *pplist)
{
newnode->next = pos;
*pplist = newnode;
}
else
{
SLTNode* prev = NULL; //前驱
SLTNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
附图解:
分析: 时间复杂度:O(N);空间复杂度:O(1)。
9 在pos位置之后插入一个结点
代码实现:
//单链表在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
//注意顺序
newnode->next = pos->next;
pos->next = newnode;
}
附图解:
10 删除pos位置前的一个结点
代码实现:
//删除pos之前的结点
void SListEraseFront(SLTNode** pplist, SLTNode* pos)
{
assert(pos);
assert(pos != *pplist);
if (pos== (*pplist)->next) //头删,第一种情况
{
free(*pplist);
(*pplist) = pos;
return;
}
SLTNode* cur = *pplist;
while (cur)
{
if (cur->next->next == pos) //找到pos前面的第二个结点
{
free(cur->next);
cur->next = pos; //链接
return;
}
cur = cur->next;
}
}
附图解:
分析: 时间复杂度:O(N);空间复杂度:O(1)。
11 删除pos位置后的一个结点
代码实现:
//如果需要在一个无头单链表的某一个结点的前面插入一个值x,如何插入?
//思路:先后插,然后跟他前一个结点的值(data)进行交换即可
//删除pos后的一个结点
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
//pos后面无结点便没有意义
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next;
pos->next = next->next; //链接
free(next);
next = NULL;
}
}
附图解:
分析: 时间复杂度:O(1);空间复杂度:O(1)。
三、源代码
Slist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//方便存储其他类型
typedef int SLTDataType;
//定义一个链表结点
typedef struct SListNode
{
SLTDataType data; //数据域
struct SListNode* next; //指针域
}SLTNode;
//单向+不带头+循环
//尾插
void SListPushBack(SLTNode** plist, SLTDataType x);
//头插
void SListPushFront(SLTNode** pplist, SLTDataType x);
//尾删
void SListPopBack(SLTNode** pplist);
//头删
void SListPopFront(SLTNode** pplist);
//查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x);
//单链表在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//并不建议在之前插入,麻烦
//单链表在pos位置之前插入(一般直接写Insert就是在之前插入)
void SListInsert(SLTNode** plist, SLTNode* pos, SLTDataType x);
//删除pos之前的结点
void SListEraseFront(SLTNode** pplist, SLTNode* pos)
//删除pos后的一个结点
void SListEraseAfter(SLTNode* pos);
//打印
void SListPrint(SLTNode* plist);
Slist.c
#include"Slist.h"
//打印
void SListPrint(SLTNode* plist)
{
assert(plist); //断言
SLTNode* cur = plist;//让cur指向头结点进行遍历
while (cur != NULL)//注:条件不是cur->next,当cur->next为空便不进入循环,尾元素访问不到
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//创建结点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
node->data = x;
node->next = NULL;
return node;
}
//头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
//为什么是**pplist?因为我们要改变的是一级指针plist,所以传参进来应该是一个二级指针(传址调用)
{
//如果传入的是空指针也可以正常插入
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
//头删
void SListPopFront(SLTNode** pplist)
{
//1.没有结点的情况
if (*pplist == NULL)
{
return;
}
//2.多个结点的情况
else
{
//保存一份下一个元素的地址(否则free过后就找不到了)
SLTNode* next = (*pplist)->next; //需要加括号,因为*和->优先级相同
free(*pplist);
*pplist = next;
}
}
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
//创建一个新结点
SLTNode* newnode = BuySLTNode(x);
//如果链表为空,让尾插的结点变成该表的第一个元素
if (*pplist == NULL)
{
*pplist = newnode;
}
else //非空, 直接尾插
{
//先遍历,找尾结点
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
//该新结点定义时就指向NULL了,所以让tail的next指向该结点即可
}
}
//尾删
void SListPopBack(SLTNode** pplist)
{
//1.没有结点的情况
if (*pplist == NULL)
{
return;
}
//2.只有一个结点的情况
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
//3.多个结点的情况
else
{
//前驱指针
SLTNode* prev = NULL;
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
//找尾结点的前驱结点
prev = tail;
//找尾结点
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
/查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist; //遍历查找
//while (cur != NULL)
while(cur)
{
if (cur->data == x)
return cur; //返回结点指针
else
cur = cur->next;
}
return NULL; //没找到,返回NULL
}
//单链表在pos位置之前插入 (麻烦,不建议使用)
void SListInsert(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
//在第一个位置插入 => 相当于头插
if (pos == *pplist)
{
newnode->next = pos;
*pplist = newnode;
}
else
{
SLTNode* prev = NULL; //前驱
SLTNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//单链表在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
//注意顺序
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos之前的结点
void SListEraseFront(SLTNode** pplist, SLTNode* pos)
{
assert(pos);
assert(pos != *pplist);
if (pos== (*pplist)->next) //头删,第一种情况
{
free(*pplist);
(*pplist) = pos;
return;
}
SLTNode* cur = *pplist;
while (cur)
{
if (cur->next->next == pos) //找到pos前面的第二个结点
{
free(cur->next);
cur->next = pos; //链接
return;
}
cur = cur->next;
}
}
//删除pos后的一个结点
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
//pos后面无结点便没有意义
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next;
pos->next = next->next; //链接
free(next);
next = NULL;
}
}
test.c
#include "Slist.h"
//尾插、头插、头删、尾删
void TestSList1()
{
SLTNode * plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
//SListPrint(plist);//1->2->3->4->NULL
SListPushFront(&plist, 0);
//SListPrint(plist);//0->1->2->3->4->NULL
SListPopBack(&plist);
//SListPrint(plist);//0->1->2->3->NULL
SListPopFront(&plist);
SListPrint(plist);//1->2->3->NULL
}
//查找
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SLTNode* pos = SListFind(plist, 3);
if (pos != NULL)
{
printf("找到了\n");
}
else
{
printf("没找到\n");
}
//找到了
//同时,可以通过pos对该位置的值进行修改
pos->data = 10;
SListPrint(plist);//1->2->10->4->NULL
}
//在pos之后、之前插入结点,删除po之前、之后的结点
void TestSList3()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SLTNode* pos = SListFind(plist,3);
SListInsertAfter(pos, 30);
//SListPrint(plist); //1->2->3->30->4->NULL
SListInsert(&plist, pos, 300);
//SListPrint(plist); //1->2->300->3->30->4->NULL
SListEraseAfter(pos);
//SListPrint(plist); //1->2->300->3->4->NULL
SListEraseFront(&plist,3);
//SListPrint(plist); //1->2->3->4->NULL
}
int main()
{
TestSList3();
return 0;
}
如果需要在一个无头单链表的某一个结点的前面插入一个值x,如何插入?
思路:先后插,然后跟他前一个结点的值(data)进行交换即可。
四、链表面试题
1 移除链表元素
题目描述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。【注 : 若未明确说明,题目所给单链表都是不带头单链表】
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while(cur)
{
if(cur->val == val)
{
struct ListNode* next = cur->next;
//如果第一个元素就需要删除,则需要单独处理
//该情况下,cur是头
if(prev == NULL)
{
//删除该结点, cur和head指向下一个结点
free(cur);
head = next;
cur = next;
}
else
{
//删除该结点
prev->next = next;
free(cur);
//cur指向被删除结点的下一个结点
cur = next;
}
}
else
{
prev = cur;
cur = cur->next;
}
}
return head;
}
2 反转链表
题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//思路一:直接使用三个指针翻转
//eg:1->2->3->4->NULL
//=> NULL<- 1 <- 2 <- 3 <- 4
//最开始 : n1指向NULL,n2指向头,n3指向头的下一个结点
//每一次都让n2指向n1(n3用于记录下次n2的位置),再挪动n1、n2、n3
//当n2为空时循环结束(画图理解),此时的n1就是反转链表的头
struct ListNode* reverseList(struct ListNode* head)
{
//如果是空链表或者只有一个结点,直接返回
if(head == NULL || head->next == NULL)
return head;
struct ListNode* n1 = NULL;
struct ListNode* n2 = head;
struct ListNode* n3 = head->next;
while(n2)
{
//翻转
n2->next = n1;
//迭代
n1 = n2;
n2 = n3;
if(n3)//当执行到最后一次时,n3已经为空,如果继续执行n3->next就会出错,所以需要做判断
n3 = n3->next;
}
return n1;
}
//思路二: 头插法 (注: 这里的头插并不创建新结点)
//取原链表中的结点,头插到新结点
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始终指向当前的头
newHead=cur;
//迭代
cur=next;
}
return newHead;
}
3 链表的中间结点
题目描述:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
/**
* 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个结点
题目描述:输入一个链表,输出该链表中倒数第k个结点。
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
struct ListNode* fast = pListHead, *slow = pListHead;
//fast先走k步
while(k--)
{
//要注意k比链表总长度还要大的情况,以及链表为空的情况
if(fast == NULL)
{
return NULL;
}
fast = fast->next;
}
while(fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
5 链表分割
题目描述:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
ListNode* partition(ListNode* pHead, int x)
{
//开辟两个链表的头结点
ListNode* lessHead, *lessTail, *greaterHead, *greaterTail;
lessHead = lessTail =(struct ListNode*)malloc(sizeof(struct ListNode));
greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
lessTail->next = NULL;
greaterTail->next = NULL;
struct ListNode* cur = pHead;
//分成两个链表
while(cur)
{
if(cur->val < x)
{
lessTail->next = cur;
lessTail = lessTail->next;
}
else
{
greaterTail->next = cur;
greaterTail = greaterTail->next;
}
cur = cur->next;
}
//链接两个链表
lessTail->next = greaterHead->next;
//关键
greaterTail->next = NULL;//很重要的一步,不然会出现死循环
pHead = lessHead->next;
free(lessHead);
free(greaterHead);
return pHead;
}
注:在链接两个链表的时候(L2链接到L1),要注意要把L2的尾指针的next指向空。因为L2是由原链表分出来的,L2的尾结点可能并不是原链表的尾结点,其后面可能还链接着其它结点,如果next不置空可能会出现死循环。
五、链表与顺序表的对比
1 链表的优缺点
1.1 优点
- 按需申请内存,需要存一个数据就申请一块内存,不存在空间浪费。
- 任意位置O(1)时间插入删除数据
1.2 缺点
- 不支持下标的随机访问
2 顺序表的优缺点
2.1 优点
- 空间连续,可以按下标进行随机访问;
- 顺序表的CPU高速缓存命中率比较高。
2.2 缺点
- 空间不够需要增容,增容有一定的性能消耗,且可能存在一定的空间浪费;
- 头部或者中间插入删除数据,需要挪动数据,效率比较低 -> O(N)。
总结:这两个数据结构是相辅相成的,他们互相弥补对方的缺点,需要用谁存数据,需要看具体场景。
今天分享的内容已经结束啦,希望此篇文章可以给你们带来方向。如果博主的分享对大家有所帮助,记得给博主一个三连哈,你的支持是我创作的最大动力!夜色难免黑凉,前行必有曙光,漫漫亦灿灿!与君共勉~ 我们下期再见。