单链表(Single Link Table)——单文件实现

简介: 单链表(Single Link Table)——单文件实现

一、单链表前言



上篇文章我们讲述了顺序表,认真学习我们会发现顺序表优缺点。


缺点1:头部和中部的插入删除效率都不行,时间和空间复杂度都为O(N);


缺点2:空间不够了扩容有一定的消耗(尤其是realloc的异地扩容);


缺点3:扩容逻辑,可能还存在空间,就像我只需要插入170个数据,但是要扩容200个数据大小,就会造成浪费。


优点1:尾插尾删足够快;


优点2:下标的随机访问和修改很快很方便;


基于顺序表的缺点我们通过今天的单链表来解决;


二、链表


2.1链表的基本概念和结构


概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。


结构图:

10df9320e0fb4a0f9dc2982cf394f476.png

链表和顺序表一样也是通过结构体实现的,不一样的是链表中的结构体只含有有效数据和指向下个结构体的结构体指针


让我们先通过代码实现一个简单的链表吧!

#include<stdio.h>
#include<stdlib.h> 
typedef struct SListNode{
  int data;
  struct SListNode* next;
}SLTNode;
int main()
{
  SLTNode*n1=(SLTNode*)malloc(sizeof(SLTNode));
  n1->data=1;
  SLTNode*n2=(SLTNode*)malloc(sizeof(SLTNode));
  n2->data=2;
  SLTNode*n3=(SLTNode*)malloc(sizeof(SLTNode));
  n3->data=3;
  n1->next=n2;
  n2->next=n3;
  n3->next=NULL;
  SLTNode*plist=n1;
  while(plist)
  {
    printf("%d->",plist->data);
    plist=plist->next;
  }
  printf("NULL\n");
   return 0;
 } 


ae7651e2863549329b99181d2a2c75e9.png


这里我们开辟了三个结构体的空间用三个结构体指针指向每个结构体,然后将每个结构体里的结构体指针指向下一个结构体,再将每个结构体存入有效数据,一个简单的3长度的链表。

注意:

1.看上面的图逻辑上我们可能会认为是连续的,但是在物理地址内存上可能是连续的也可能是不连续的;

2.现实中的每个结点都是从堆上开辟的

3.从堆上开辟空间是按照一定策略的,两次连续的开辟空间可能是连续的,也可能是不连续的


2.2链表的分类


2.2.1单向或者双向

78bbac9c6b33477ab59cb9772141cf37.png

2.2.2带头或者不带头

cdcd00b72a5e45db8e628ac1c1c94fcb.png

2.2.3循环或者非循环


5479b0f02aac4eed9783c550ce901c39.png

虽然有这么多的链表结构,但是我们实际中最常用的还是两种结构


70f77b5a2a3341bca0dc26fa6cb106dd.png

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结

构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都

是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带

来很多优势,实现反而简单了,后面我们代码实现了就知道了。

三、无头单向非循环链表增删查改链表的实现


3.1链表的创建


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


3.2填充数据及开辟新的结点


SLTNode* BuySLT(SLTDatatype x)
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc failed");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}

链表的实现靠的是结构体里的结构体指针指向下一个结构体链接,所以我们要将新的结点返回,

返回的是结构体指针,定义函数是也要结构体指针接受。


3.3打印链表


void SLprint(SLTNode* phead)
{
  SLTNode* cur = phead;
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}

我们是靠头结构体指针找到链表的,但是打印有效数据要移动指针所以我们就要在一开始创建一个指针,利用这个指针的移动打印数据,当这个指针为空时并不是指向空时停止打印。


3.4查找有效数据


SLTNode* SLFind(SLTNode* phead, SLTDatatype x)
{
  SLTNode* cur = phead;
  while (cur)
  {
    if (cur->data = x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}

和打印有效数据相似通过另一个指针的移动查找我们需要的有效数据,当指针为空时我们找不到有效数据返回NULL,当找到有效数据时返回指向含有这个有效数据的结构体;


3.5单链表尾插


void SLTPushBack(SLTNode** pphead, SLTDatatype x)
{
  SLTNode* newnode = BuySLT(x);
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    SLTNode* tail = *pphead;
    while (tail->next)
    {
      tail = tail->next;
    }
    tail->next = newnode;
  }
}

首先我们需要一个空结构体指针指向开辟好的返回值为结构体指针,当无链表时也相当于头插我们只需将开辟好返回值为结构体指针赋值给空指针就行,此时这个链表只有一个结构体;当链表已经形成时此时才是我们的尾插,我们需要另一个指针从头开始找到链表末尾的空指针,再将这个空指针指向以开辟好的返回值为结构体指针就可以从尾部将其串联起来;


为什么要使用二级指针呢?


我们是有一个指针指向空或者链表的头,在尾插时我们要通过头指针的移动增添数据,但是这个指针我们不可以移动,移动的话会造成内存泄漏,所以我们又创建一个指针将这个指针指向我们的空或者链表的头,通过指针的操作将我们的链表串联起来,我们也可以将这个指针想象成我们的头指针,这个指针会变化相当于头指针变化,但是我们是在一个函数域中操作,除了这个作用域一切都会销毁,相当于什么都没敢。通俗的说我们是在操作头指针,在另个作用域中改变我们原有的值只能通过二级指针实现,但是头指针已经是个指针了,所以我们需要一个二级指针。


3.6尾删


void SLTPopBack(SLTNode** pphead)
{
  if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
        SLTNode* tail = *pphead;
    while (tail->next->next)
    {
      tail = tail->next;
    }
    free(tail->next);
    tail->next = NULL;
  }
}

首先我们要判断这个链表是否只有一个结构体,如果只有一个结构体直接将这个结构体释放销毁就行了;如果有多个结构体链接只需要找到倒数第二个结构体中的指向最后一个结构体的指针,释放这个指向最后一个结构体的指针就是可以实现尾删;


3.7头插

void SLPushFront(SLTNode** pphead, SLTDatatype x)
{
  SLTNode* newnode = BuySLT(x);
  if ((*pphead) == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    SLTNode* newnode = BuySLT(x);
    newnode->next = *pphead;
    *pphead = newnode;
  }
}

如果这个头指针为空时及没有链表,直接将这个指针指向开辟好的结构体的就行,如果不为空,将开辟好的结构体里指向下一个结构体的结构体指针指向我们的头指针,再将头指针指向开辟好的结构体就行;


3.8头删


void SLPopFront(SLTNode** pphead)
{
  if ((*pphead) == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    SLTNode* newhead = (*pphead)->next;
    free(*pphead);
    *pphead = newhead;
  }
}

首先我们还是先判断这个链表是否只含有一个结构体,如果只含有一个结构体和尾删的方法一样


如果含有多个结构体头删,我们只需要新创建一个指针保存第一个结构体里储存着第二个结构体的指针,然后释放掉我们的头指针,再将新创建的指针赋值给头指针就可以完成尾删。


到这里无头单向非循环链表增删查改的重点内容和注意事项就讲解完毕了,下面我们实现一下链表中间随机的增删查改,就不一一讲解了,大家自行了解;


四、无头单向非循环链表中间随机增删查改


4.1在pos位置后插入x


void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{
  SLTNode* newnode = BuySLT(x);
  newnode->next = pos->next;
  pos->next = newnode;
}


4.2在pos位置后插入x


void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
  if (pos == *pphead)
  {
    SLPushFront(pphead, x);
  }
  else
  {
    SLTNode* newnode = BuySLT(x);
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = newnode;
    newnode->next = pos;
  }
}


4.3删除pos位置的值


void SLTErase(SLTNode** pphead, SLTNode* pos)
{
  if ((*pphead) == pos)
  {
    SLPopFront(pphead);
  }
  else
  {
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}


4.4删除pos位置后面的值


void SLEraseAfter(SLTNode* pos)
{
  assert(pos->next);
  SLTNode* newnode = pos->next->next;
  free(pos->next);
  pos->next = newnode;
}


五、无头单向非循环链表增删查改完整代码


#define _CRT_SECURE_NO_WARNINGS 67
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDatatype;
typedef struct SListNode
{
  SLTDatatype data;
  struct SListNode* next;
}SLTNode;
//打印 
void SLprint(SLTNode* phead)
{
  SLTNode* cur = phead;
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}
//开辟新的空间
SLTNode* BuySLT(SLTDatatype x)
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc failed");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}
//查找
SLTNode* SLFind(SLTNode* phead, SLTDatatype x)
{
  SLTNode* cur = phead;
  while (cur)
  {
    if (cur->data = x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDatatype x)
{
  SLTNode* newnode = BuySLT(x);
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    SLTNode* tail = *pphead;
    while (tail->next)
    {
      tail = tail->next;
    }
    tail->next = newnode;
  }
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
  if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    SLTNode* tail = *pphead;
    while (tail->next->next)
    {
      tail = tail->next;
    }
    free(tail->next);
    tail->next = NULL;
  }
}
//头插  
void SLPushFront(SLTNode** pphead, SLTDatatype x)
{
  SLTNode* newnode = BuySLT(x);
  if ((*pphead) == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    SLTNode* newnode = BuySLT(x);
    newnode->next = *pphead;
    *pphead = newnode;
  }
}
//头删 
void SLPopFront(SLTNode** pphead)
{
  if ((*pphead) == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    SLTNode* newhead = (*pphead)->next;
    free(*pphead);
    *pphead = newhead;
  }
}
//在pos位置后插入x
void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{
  SLTNode* newnode = BuySLT(x);
  newnode->next = pos->next;
  pos->next = newnode;
}
//在pos位置前插入x 
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
  if (pos == *pphead)
  {
    SLPushFront(pphead, x);
  }
  else
  {
    SLTNode* newnode = BuySLT(x);
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = newnode;
    newnode->next = pos;
  }
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
  if ((*pphead) == pos)
  {
    SLPopFront(pphead);
  }
  else
  {
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}
void SLEraseAfter(SLTNode* pos)
{
  assert(pos->next);
  SLTNode* newnode = pos->next->next;
  free(pos->next);
  pos->next = newnode;
}
int main()
{
  SLTNode* plist = NULL;
  //尾插
  SLTPushBack(&plist, 1);
  SLTPushBack(&plist, 2);
  SLTPushBack(&plist, 3);
  SLTPushBack(&plist, 4);
  SLTPushBack(&plist, 5);
  SLTPushBack(&plist, 10);
  printf("尾插:\n");
  SLprint(plist);
  //尾删
  printf("尾删:\n");
  SLTPopBack(&plist);
  SLprint(plist);
  //头插
  printf("头插:\n");
  SLPushFront(&plist, 10);
  SLprint(plist);
  //头删
  printf("头删:\n");
  SLPopFront(&plist);
  SLprint(plist);
  //在pos位置后插入x
  SLTNode* pos1 = SLFind(plist, 1);
  SLInsertAfter(pos1, 10);
  printf("在pos位置后插入x\n");
  SLprint(plist);
  //在pos位置前插入x
  SLTNode* pos2 = SLFind(plist, 1);
  printf("在pos位置前插入x\n");
  SLInsert(&plist, pos2, 20);
  SLprint(plist);
  //删除pos位置的值
  SLTNode* pos3 = SLFind(plist, 10);
  SLTErase(&plist, pos3);
  printf("删除pos位置的值:\n");
  SLprint(plist);
  //删除pos后面的值
  SLTNode* pos4 = SLFind(plist, 1);
  SLEraseAfter(pos4);
  printf("删除pos位置后面的值:\n");
  SLprint(pos4);
  return 0;
}


21600e7bcae348d98a85a7f4df4cacdd.png

到这里我们的无头单向非循环链表增删查改的所有内容就讲解完了,有什么问题或者需要指正的可以在评论区留下您宝贵的意见。

相关文章
Leetcode 623. Add One Row to Tree
题目很简单,在树的第d层加一层,值为v。递归增加一层就好了。代码如下
53 0
|
存储
|
存储
LeetCode 107. Binary Tree Level Order Traversal II
给定二叉树,返回其节点值的自下而上级别顺序遍历。 (即,从左到右,逐下而上)。
82 0
LeetCode 107. Binary Tree Level Order Traversal II
|
C++
C++ std::map报错的解决办法:_Rb_tree_increment(std::_Rb_tree_node_base const
C++ std::map报错的解决办法:_Rb_tree_increment(std::_Rb_tree_node_base const
1335 0
|
Windows
PE结构讲解--section table 和 section
本文为转载文章,整理自小甲鱼老师讲的PE结构课程; 一、PE文件到内存的映射: 在执行一个PE文件的时候,windows 并不在一开始就将整个文件读入内存的,而是采用与内存映射文件类似的机制。也就是说,windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。
1978 0
|
算法
[LeetCode]--102. Binary Tree Level Order Traversal
Given a binary tree, return the level order traversal of its nodes’ values. (ie, from left to right, level by level). For example: Given binary tree [3,9,20,null,null,15,7], 3 / \ 9
1248 0