身家过亿的帝都富豪来参加1024节专属盛典,小码农献上单链表一篇来庆祝盛典

简介: 身家过亿的帝都富豪来参加1024节专属盛典,小码农献上单链表一篇来庆祝盛典

文章目录


身家过亿的帝都富豪来参加1024节专属盛典,小码农献上单链表一篇来庆祝盛典

顺序表的缺陷

1.空间不够了需要扩容,增容是要付出代价

realloc的机制代价,与身具来的代价,1.后面内存足够开辟就原地扩容,2.后面内存不够就会异地扩容

2.避免频繁扩容,我们满了基本都是扩2倍,可能就会导致一定的空间浪费

3.顺序表要求数据从开始位置连续存储那么我们在头部或者中间位置插入删除数据就需要挪动数据,效率不高

针对顺序表的缺陷,就设计出了链表

链表是按需申请空间,不用了就释放空间(更合理的使用空间)

头部中间尾部插入删除数据,不需要挪动数据

当然他也是有缺陷的,例如每个数据后面都要存一个指针去链接后面的数据节点

不支持随机访问(也就是没有下标访问了)


链表

链表的概念及结构

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

image.png

链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构

1.单向或者双向

image.png

2.带头或者不带头

image.png

3.循环或者非循环

image.png

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

image.png

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:==结构最复杂,==一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而
    简单了,后面我们代码实现了就知道了。


链表的实现

无头单向

单链表节点

typedef int SLTDataType;
//单链表节点
typedef struct SListNode
{
  SLTDataType data;
  struct SListNode* next;
}SLTNode;

image.png

image.png


单链表打印函数SListPrint

有时候一步一步调试会不怎么方便所以我们先把打印函数写出来,然后再写增删改查也不迟

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


单链表尾插函数SListPushBack

只要是插入不管你如何如何反正是插入你都得扩容,当然单链表扩容的单位是节点

//单链表尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
  //申请一个新节点
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  newnode->data = x;
  newnode->next = NULL;
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    //找到尾节点
    SLTNode* tail = *pphead;
    while (tail->next != NULL)
    {
      tail = tail->next;
    }
    tail->next = newnode;
  } 
}

image.png

获得单链表节点函数

这时我是想写头插,但是思考一番发现,创建节点这一步会和尾插里面有些步骤重复,因此我们可以把重复的步骤抽离出来写成一个函数就会方便一些

//获得单链表节点函数
SLTNode* BuySListNode(SLTDataType x)
{
  //申请一个新节点
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)//实际上如果申请失败了,就说明堆没有多少空间了
  {
    printf("申请失败\n");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;//然后把节点指针返回出去
}

所以前面那个尾插就可以用这个复写了


单链表头插函数SListPushFront

//单链表头插函数
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
  //申请一个新节点
  SLTNode* newnode = BuySListNode(x);
  //让新节点里面存plist头结点的地址
  newnode->next = *pphead;
  //然后再让newnode成为plist头节点
  *pphead = newnode;
}

image.png


单链表尾删函数SListPopBack

//单链表尾删
void SListPopBack(SLTNode** pphead)
{
  assert(*pphead);
  if (!(*pphead)->next)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    SLTNode* prev = NULL;//类似一个标记节点指针
    SLTNode* tail = *pphead;
    while (tail->next)//找到最后一个节点
    {
      prev = tail;//每次操作tail前让prev先存一下他
      tail = tail->next;
    }
    free(tail);//把他空间释放了
    tail = NULL;//然后指向空
    //但是前一个next我们没有操作就是指向了原来已经释放掉了的空间
    prev->next = NULL;
  } 
}

image.png


单链表头删函数SListPopFront

//单链表头删
void SListPopFront(SLTNode** pphead)
{
  assert(*pphead);
  SLTNode* next = (*pphead)->next;
  free(*pphead);
  (*pphead) = next;
}

image.png


单链表查找函数

//单链表查找函数
SLTNode* SListFind(SLTNode* phead,SLTDataType x)
{
  SLTNode* cur = phead;
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    else
    {
      cur = cur->next;
    }
  } 
  return NULL;
}

主文件的写法

void test1()
{
  SLTNode* plist = NULL;
  SListPushBack(&plist, 1);
  SListPushBack(&plist, 2);
  SListPushBack(&plist, 3);
  SListPushBack(&plist, 4);
  SListPushFront(&plist, 1);
  SListPushBack(&plist, 4);
  SListPushFront(&plist, 2);
  SListPushBack(&plist, 4);
  SListPushFront(&plist, 3);
  SListPushBack(&plist, 4);
  SListPushFront(&plist, 4);
  SListPrint(plist);
  SLTNode* pos =  SListFind(plist,4);
  int i = 1;
  while (pos)
  {
    printf("第%d个pos节点,%p->%d\n",i++,pos,pos->data);
    //找到后的下一个地址
    pos = SListFind(pos->next, 4);
  }
}

image.png


查询后修改

  SLTNode* pos1 = SListFind(plist,2);
  if (pos1)
  {
    //这里就可以看到查询返回节点指针的价值所在了
    pos1->data = 20;
  }
  SListPrint(plist);

image.png


单链表插入函数

可以通过和查询函数配合来进行插入

//单链表插入函数
//在pos前面插入,这个pos在哪里来呢,就是从前面查找函数来
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
  //首先创建节点
  SLTNode* newnode = BuySListNode(x);
  if (pos == *pphead)
  {
    newnode->next = *pphead;
    *pphead = newnode;
  }
  else
  {
    //找到pos的前一个位置
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = newnode;
    newnode->next = pos;
  } 
}

image.png

看到上面就是知道单链表不太适合在前面插入,而是适合在后面插入,因为前插你要知道当前位置前面的那一个节点位置就会有点复杂,而后插就没有这个prev的节点指针了

单链表后插函数

//单链表后插函数
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
  //创建一个新的节点
  SLTNode* newnode = BuySListNode(x);
  newnode->next = pos->next;
  pos->next = newnode;
}

image.png

单链表删除函数SListErase

单链表删除函数
这是我自己写的,我们看下面老师写的
//void SListErase(SLTNode** pphead, SLTNode* pos)
//{
//  SLTNode* cur = *pphead;
//  SLTNode* prev = NULL;
//  while (cur)
//  {
//    if (cur == pos)
//    {
//      if (pos == *pphead)
//      {
//        cur = (*pphead)->next;
//        free(*pphead);
//        (*pphead) = NULL;
//        *pphead = cur;
//      }
//      else
//      {
//        prev->next = cur->next;
//        free(cur);
//        cur = prev->next;
//      }     
//    }
//    else
//    {
//      prev = cur;
//      cur = cur->next;
//    }
//  }
//}
//单链表删除函数
//老师写的
void SListErase(SLTNode** pphead, SLTNode* pos)
{
  if (pos == (*pphead))
  {
    //头删函数
    SListPopFront(pphead);
  }
  else
  {
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}

image.png

当然还有简洁的后删函数

单链表后删函数

//单链表后删函数
void SListEraseAfter(SLTNode* pos)
{
  assert(pos->next);
  SLTNode* nextnode = pos->next;
  pos->next = nextnode->next;
  free(nextnode);
}

image.png

单链表销毁函数

//单链表销毁函数
void SListDestory(SLTNode** pphead)
{
  SLTNode* cur = *pphead;
  while (cur)
  {
    SLTNode* nextnode = cur->next;
    free(cur);
    cur = nextnode;
  }
  *pphead = NULL;
}


练习题(从小到大老师一直教我们数形结合这东西是逻辑的根本,往往简单的东西有人掌握了就变成了厉害人物,反而那些不在意图的,死在途中无人问津,曾经我也是喜欢走的快,但是现在我放慢了脚步,为了走得远

1.移除链表元素

image.png

图解

image.png

image.png

image.png

struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* pos  = head;
    struct ListNode* prev = NULL;    
    while(pos)
    {
        if(pos->val == val)
        {
            if(pos == head)
            {
                pos = head->next;
                free(head);
                head = pos;
            }
            else
            {
                prev->next = pos->next;
                free(pos);
                pos = prev->next;
            }           
        }
        else
        {
            prev = pos;
            pos = pos->next;
        }
    }
    return head;
}


2.反转链表

image.png

image.png

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* prev = NULL;
    struct ListNode* cur = head;
    if(!cur)
    {
        return head;
    }
    else
    {
        struct ListNode* next = head->next;
        while(next)
        {       
            cur->next = prev;
            prev = cur;
            cur = next;  
            next = next->next;     
        }   
        cur->next = prev;
        return cur;
    }    
}

3.链表的中间结点

image.png

image.png


struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while((fast != NULL)&&(fast->next != NULL))
    {
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}


4.链表中倒数第k个结点

image.png

image.png

image.png

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


5.合并两个有序链表

image.png

image.png

image.png

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
    struct ListNode* head = NULL;
    struct ListNode* tail = NULL;
    if(!l1)
        return l2;
    if(!l2)
        return l1;    
    while(l1&&l2)
    {
        if(l1->val <= l2->val)
        {
            if(!head)
            {
                head = tail = l1;
            }
            else
            {
                tail->next = l1;
                tail = l1;
            }
            l1 = l1->next;
        } 
        else
        {
            if(!head)
            {
                head = tail = l2;
            }
            else
            {
                tail->next = l2;
                tail = l2;
            }
            l2 = l2->next;
        }
    }
    tail->next = (l1 == NULL) ? l2 : l1;
    return head;       
}


目录
相关文章
|
7月前
|
Java 程序员 微服务
学历不够,技术来凑,看八年开发码农如何逆袭进阿里拿年薪百万
有人说,今年可能是过去十年最差的一年,但却是未来十年最好的一年。随着越来越多的知名企业进行大规模裁员,我们不得不承认一个事实:经济寒冬与裁员潮,将是未来常态!
广东工业大学第十四届程序设计竞赛 1004免费送气球(线段树)
广东工业大学第十四届程序设计竞赛 1004免费送气球(线段树)
82 0
|
算法
2021-2022年度第三届全国大学生算法设计与编程挑战赛(秋季赛)热身赛 B.这是一道大水题(树状数组)
2021-2022年度第三届全国大学生算法设计与编程挑战赛(秋季赛)热身赛 B.这是一道大水题(树状数组)
163 0
2021-2022年度第三届全国大学生算法设计与编程挑战赛(秋季赛)热身赛 B.这是一道大水题(树状数组)
|
Shell 计算机视觉
2021年中国大学生程序设计竞赛女生专场C. 连锁商店(状压dp 思维 贪心)
2021年中国大学生程序设计竞赛女生专场C. 连锁商店(状压dp 思维 贪心)
159 0
|
存储 Linux
身价过亿的帝都富豪对小码农说顺序表会了吗
身价过亿的帝都富豪对小码农说顺序表会了吗
152 0
身价过亿的帝都富豪对小码农说顺序表会了吗
|
编译器 Linux 程序员
身价过亿的帝都富豪对小码农说预处理学的不错
身价过亿的帝都富豪对小码农说预处理学的不错
186 0
身价过亿的帝都富豪对小码农说预处理学的不错
|
新零售 大数据 物联网
发自肺腑深入肌肤 —— 一位武汉老程序员的自白
我是一个对技术没有很大热情的程序员。 即使在项目忙的时候我也不会加班很长时间,因为我觉得我的身体坐了一天了,它予我以生存,我必须善待它,但步行3公里回去吃完饭我还是会在各论坛上看看解决问题的最好办法,因为公司予我以饭碗,我必须对得起他,不断的学习只是因为单纯的觉得想要更好就必须学习,出于欲望而不是热情有时会走不多远但又强迫自己继续学习,上班的代码是工作的,下班的代码是自己的,还是会不断的焦虑,imooc1200多小时不够、实验楼七十多层不够、网易云课堂几百个课时不够、leetcode刷一遍不够……前路其修远兮,吾心何处安放。
1538 0
|
程序员 前端开发
北漂程序员的辛酸:年薪30多万,却活得像乞丐一样
程序员应该算是很光鲜的行业了,也是其他职业人士所羡慕和向往的。然而生活就像围城一样,不在其中不知其滋味,特别是在北上广深一带打拼的程序员,其生活状况可能没有我们想象的那么光鲜。
1013 0
下一篇
DataWorks