目录
1->认识链表
先认识下线性表:
线性表(linear list)是n个具有相同特性的数据元素的有限序列. 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储.
看图认识下结构
再来思考一个问题:
顺序表的问题及思考:
问题:
(1)顺序表进行中间/头部的插入删除,时间复杂度为O(N)
(2)增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗
(3)增容一般是呈2倍的增长,势必会有一定的空间浪费,例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间思考:
怎么解决顺序表的这些问题呢?没错,就是用我们今天讲解的链表!
接下来认识链表:
链表的定义:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
看下链表的逻辑结构,我们写题和写代码的时候就这样用
也可以这样理解
注意:
(1)从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
(2)现实中的结点一般都是从堆上申请的
(3)从堆上申请的空间,是按照一定的策略来分配的,连续两次申请的空间可能连续,也可能不连续
2->链表种类以及最常用的两种链表
实际开发中链表的种类繁多,大体就8类,以下3个组合即可
- 单向 双向
提供两种图,你自己看哪个好理解就用哪个
- 带头结点 不带头结点
- 循环 非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等.另外这种结构在笔试面试中出现很多.
2. 带头双向循环链表: 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了.
3->模拟实现单链表--重头戏,请务必掌握
3.1自定义链表结点 struct SListNode
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
//用C语言模拟实现"不带头单向不循环链表,插入数据以int为例子
//定义单链表结构体
typedef struct SListNode
{
int _data; //数据域
struct SListNode* _next;//指针域
}SLTNode;
3.2动态申请一个链表节点,别忘记删除的时候释放哦
//动态申请一个单链表节点
SListNode* CreateSLTNode(int x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc failed申请空间失败,我要退出哦!\n");
exit(1);
}
newnode->_data = x;
newnode->_next = NULL;
return newnode;
}
3.3链表打印数据
//链表打印数据
void SListPrint(SLTNode* plist)
{
//assert(plist);
if (plist == NULL)
{
printf("链表为空 : ");
}
SLTNode* cur = plist;
while(cur != NULL)
{
printf("%d--->",cur->_data);
cur = cur->_next;
}
printf("NULL\n");//最后一个节点指向NULL
}
3.4链表增删改查之尾部插入数据
//接下来实现链表的增删查改
//链表的尾部插入
void SListPushBack(SLTNode** pplist,int num)
{
assert(pplist);
SLTNode* newnode = CreateSLTNode(num);//不管哪种情况,节点要先申请出来
//考虑刚开始插入数据一个节点都没有的情况
if (*pplist == NULL)
{
*pplist = newnode;
}
else//已经存在节点的情况
{
SLTNode* tail = *pplist;
while (tail->_next != NULL)
{
tail = tail->_next;
}
tail->_next = newnode;
}
}
我们写代码测试下是否正确:
//测试尾部插入
void test1()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushBack(&plist, 5);
SListPrint(plist);
}
看控制台输出:结果正确
3.5链表增删改查之头部插入数据
//链表的头部插入
void SListPushFront(SLTNode** pplist,int num)
{
//头部插入就很简单了,不需要考虑有节点或无节点的两种情况
//先创建节点
SLTNode* newnode = CreateSLTNode(num);
newnode->_next = *pplist;
*pplist = newnode;
}
我们写代码测试下是否正确:
//测试头部插入
void test2()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPrint(plist);
}
看控制台输出:结果正确
3.6链表增删改查之尾部删除数据
先看我画的图理解下删除前后的细节
下边是实现的代码
//链表的尾部删除
void SListPopBack(SLTNode** pplist)
{
SLTNode* plist = *pplist;//获得链表起始地址
//1.没有节点
if (plist == NULL)
{
printf("目前没有节点,不能删除,程序退出!!!\n");
exit(666);
}
else if (plist->_next == NULL)//2.一个节点
{
//只有一个节点,释放最后一个节点,将链表指针置空
free(plist);
plist = NULL;
*pplist = NULL;
}
else//3.>=2 多个节点
{
//首先找到尾部以及尾部
SLTNode* prev_tail = *pplist;
SLTNode* tail = *pplist;
while (tail->_next != NULL)
{
prev_tail = tail;
tail = tail->_next;
}
//走到这里的时候,tail指向最后一个节点,prev_tail指向倒数第二个节点
free(tail);
prev_tail->_next = NULL;
}
}
我们写代码测试下是否正确:
//测试尾部删除
void test3()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPrint(plist);
SListPopBack(&plist);
printf("plist->%p\n",plist);
SListPrint(plist);
}
看控制台输出:结果正确
3.7链表增删改查之头部删除数据
先看我画的图理解下删除前后的细节
下边是实现的代码
//链表的头部删除
void SListPopFront(SLTNode** pplist)
{
//链表头部删除肯定会导致链表指针plist变化
SLTNode* plist = *pplist;//获得链表起始地址
//1.没有节点
if (plist == NULL)
{
printf("目前没有节点,不能删除,程序退出!!!\n");
exit(666);
}
else//2.有节点,那就大胆删除无需考虑有几个节点
{
SLTNode* next = plist->_next;
free(plist);
*pplist = next;
}
}
我们写代码测试下是否正确:
//测试头部删除
void test4()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
}
看控制台输出:结果正确
3.8链表增删改查之查找数据
//链表查找数据
SLTNode* SListSearch(SLTNode* plist, int num)
{
if (plist == NULL)
{
printf("目前没有节点,不能查找数据,程序退出!!!\n");
exit(666);
}
SLTNode* cur = plist;
while (cur != NULL)
{
if (cur->_data == num)
{
return cur;
}
cur = cur->_next;
}
return NULL;//到这里说明要找的数据在这个链表里不存在
}
我们写代码测试下是否正确:
//测试查找数据
void test5()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SLTNode* ptr = SListSearch(plist,3);
printf("ptr->%d\n",ptr->_data);
}
看控制台输出:结果正确
3.9链表增删改查之在pos位置后边插入数据
//在pos位置后边插入数据
void SListInsertAfter(SLTNode* pos,int num)
{
assert(pos);
SLTNode* newnode = CreateSLTNode(num);
newnode->_next = pos->_next;
pos->_next = newnode;
}
我们写代码测试下是否正确:
//测试pos后插入数据
void test6()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPrint(plist);
SLTNode* ptr = SListSearch(plist, 3);
SListInsertAfter(ptr,666);
SListPrint(plist);
}
看控制台输出:结果正确
3.10链表增删改查之在pos位置前边插入数据
//在pos位置前边插入数据
void SListInsertBefore(SLTNode** pplist,SLTNode* pos,int num)
{
assert(pplist && pos);
if (pos == *pplist)//说明要插入的位置是链表的头部
{
//这里就可以偷懒,用我们之前写的头部插入
SListPushFront(pplist, num);
}
else
{
SLTNode* prev = NULL;
SLTNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->_next;
}
//到这里cur == pos,prev指向pos的前一个节点
SLTNode* newnode = CreateSLTNode(num);
newnode->_next = pos;
prev->_next = newnode;
}
}
我们写代码测试下是否正确:
//测试pos前插入数据
void test7()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListInsertBefore(&plist,plist,10);
SListPushFront(&plist, 2);
SListPushFront(&plist, 4);
SListPrint(plist);
SLTNode* ptr = SListSearch(plist, 4);
SListInsertBefore(&plist, ptr, 666);
SListPrint(plist);
}
看控制台输出:结果正确
3.11链表增删改查之删除pos位置的数据
// 删除pos位置的值
void SListErase(SLTNode** pplist, SLTNode* pos)
{
assert(pplist);
assert(pos);
if (pos == *pplist)//头部删除
{
SListPopFront(pplist);
}
else
{
SLTNode* prev = *pplist;
while (prev->_next != pos)
{
prev = prev->_next;
}
//到这里prev指向pos前一个位置
prev->_next = pos->_next;
free(pos);
}
}
4->您的专属鼓励师
数据结构和算法修行之路确实枯燥,但是我们把问题搞懂以后就发现他是那样的美妙!一遍学不会没关系吖,多看几遍,我也是学了好多遍呢,小伙伴们肯定学的又快又好!!!最后希望写的内容对小伙伴们有所帮助,我写的如果有哪里不对的地方请在评论区或者私信指出来哦!让我们一起进步吖,任何疑问包括心情不好都可以找我聊聊,我很乐意当你的倾听者吖.