身家过亿的帝都富豪来参加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;       
}


目录
相关文章
时隔4年再夺金奖!北大斩获「编程奥林匹克」亚军,刷新队史最高排名
时隔4年再夺金奖!北大斩获「编程奥林匹克」亚军,刷新队史最高排名
123 0
|
架构师 测试技术 区块链
Substrate 2021 年终总结盛典回顾
Substrate 可以视为一个区块链框架,其目的是帮助构建定制区块链。它使开发人员能够快速、轻松地构建面向未来的网络,这些网络几乎针对所有用例进行了优化。它免除了区块链开发的繁重工作,而且不像其他框架那样施加限制。
139 0
Substrate 2021 年终总结盛典回顾
|
存储 Linux
身价过亿的帝都富豪对小码农说顺序表会了吗
身价过亿的帝都富豪对小码农说顺序表会了吗
124 0
身价过亿的帝都富豪对小码农说顺序表会了吗
|
传感器
两个月吸金4亿美元,《原神》大奖拿到手软
众所周知,《原神》是一款颇具争议的游戏,但无论如何,从现有的成绩来看,《原神》无疑是非常成功的。近日,Sensor Tower发布了11月份中国手游发行商全球营收排行榜,此前连前十名都进不去的米哈游,如今凭借《原神》已经荣升至第三名,仅次于行业巨头的腾讯和网易。
809 0
两个月吸金4亿美元,《原神》大奖拿到手软
|
机器学习/深度学习 安全 程序员
码龄5年,17岁香港少年获苹果编程挑战赛奖项,教贫困儿童学编程
码龄5年,17岁香港少年获苹果编程挑战赛奖项,教贫困儿童学编程
203 0
|
人工智能 监控 算法
创企明星|获总理二次点赞,偶像为她投资,90后美女的开挂创业人生
90后、工学美女博士、海内外机器人竞赛冠军队队长、连续创业者、入选福布斯30Under30精英榜、受到李克强总理等国家领导人接见、创立的公司受到资本热捧。以上形容词就是对一个年仅27岁的女孩的描述——黄山。
创企明星|获总理二次点赞,偶像为她投资,90后美女的开挂创业人生