看完这篇文章还不会顺序表和链表,请寄刀片给我 下

简介: 看完这篇文章还不会顺序表和链表,请寄刀片给我

三、链表

💦 链表的概念和结构

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

🎗 这里就来实现一个简单的链表(这里有三个文件:SList.h、SList.c、Test.c)

SList.h文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
  SLTDataType data;
  struct SListNode* next;
}SLTNode;
void SListPrint(SLTNode* phead);

SList.c文件

#include"SList.h"
void SListPrint(SLTNode* phead)
{
  SLTNode* cur = phead;
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}

Test.c文件

#include"SList.h"
void TestSList1()
{
  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;
  SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
  n4->data = 4;
  n1->next = n2;
  n2->next = n3;
  n3->next = n4;
  n4->next = NULL;
  SLTNode* plist = n1;
  SListPrint(plist);
}
int main()
{
  TestSList1();
  return 0;
}

💨 结果

⚠ 注意

1️⃣ 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续

2️⃣ 现实中的结点一般都是从堆上申请出来的

3️⃣ 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

假设在32位系统上,结点中值域为int类型,则一个节点的大小为8个字节,则也可能有下述链表:

💦 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
注:本章只了解单链表

1️⃣ 单向或者双向

2️⃣ 带头或者不带头

3️⃣ 循环或者非循环


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

1️⃣ 无头单向非循环链表

2️⃣ 带头双向循环链表

⚠ 注意:

▶ 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

▶ 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

💦 单链表各接口实现 (动图分析)

这里写了三个文件:

1️⃣ SList.h文件,用于函数声明

2️⃣ SList.c文件,用于函数的定义

3️⃣ Test.c文件,用于测试函数


⭕ 首先需要定义结构体SLTNode

typedef int SLTDataType;
typedef struct SListNode
{
  SLTDataType data;//存储整型数据
  struct SListNode* next;//指向下一个节点的地址 
}SLTNode;

并定义plist变量 -> Test.c

SLTNode* plist = NULL;

⭕ 接口1:开辟空间 (BuySListNode)

函数原型:

函数实现:

SLTNode* BuySListNode(SLTDataType x)
{
  //每次调用开辟一个节点的空间
  SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
  if (node == NULL)
  {
    printf("malloc fail\n");
    exit(-1);
  }
  node->data = x;
  node->next = NULL;
  //开辟成功,返回地址
  return node;
}

⭕ 接口2:输出数据 (SListPrint)

函数原型:

函数实现:

void SListPrint(SLTNode* phead)
{
  //据情况分析,此处不需要断言,因为我们想在空链表时输出NULL
  //assert(phead);
  SLTNode* cur = phead;
  //遍历
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}

⭕ 接口3:尾插数据 (SListPushBack),详解请看下图:

函数原型:

函数实现:

void SListPushBack(SLTNode** pphead, SLTDataType x)
{
  //据析,指针可能为空,但是指针的地址不可能为空,所以需要断言(pphead就是plist的地址),且这里不能断言*pphead,因为这里空链表是可以处理的
  assert(pphead);
  //特殊情况
    //1、空链表
  if (*pphead == NULL)
  {
    //调用BuySListNode去开辟新节点
    SLTNode* newnode = BuySListNode(x);
    *pphead = newnode;
  }
    //2、非空链表
  else
  {
    SLTNode* tail = *pphead;
    //找尾 - NULL
    while(tail->next != NULL)
    {
      tail = tail->next;
    } 
    //开辟节点
    SLTNode* newnode = BuySListNode(x);
    tail->next = newnode; 
  }
}

📐 测试

⭕ 接口4:头插数据 (SListPushFront),详解请看下图:

函数原型:

函数实现:

void SListPushFront(SLTNode** pphead, SLTDataType x)
{
  assert(pphead);
  //对于首插来说,没有特殊情况,以下代码适用于空链表和非穿链表
  SLTNode* newnode = BuySListNode(x);
  newnode->next = *pphead;
  *pphead = newnode;
}

📐 测试

⭕ 接口5:尾删数据 (SListPopBack),详解请看下图(2种方式):

方式1:

方式2:

函数原型:

函数实现:

void SListPopBack(SLTNode** pphead)
{
  //据析,这里需要断言plist的地址是否传成NULL了;以及没有节点的情况
  assert(pphead);
  assert(*pphead);
  //特殊情况
  //一个节点的情况
  if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    //多个节点的情况 - 找尾 
    //1.先指向第一个节点的位置
    SLTNode* tail = *pphead;
    //2.删尾(错误示范) - 这样删尾会造成野指针(因为每个节点都是一个局部节点,如果这样释放掉后,则前一个节点的next就是一个野指针)
    /*while (tail->next != NULL)
    {
      tail = tail->next;
    }
    free(tail);
    tail = NULL; */
    //2.删尾(正确示范1)
    /*while (tail->next->next != NULL)
    { 
      tail = tail->next; 
    }
    free(tail->next);
    tail->next = NULL;*/
    //2.删尾(正确示范2) - 双指针
    SLTNode* prev = NULL;
    while (tail->next != NULL)
    {
      prev = tail;
      tail = tail->next;
    }
    free(tail->next);
    prev->next = NULL;
  }
}

📐 测试

⭕ 接口6:头删数据 (SListPopFront),详解请看下图

函数原型:

函数实现:

void SListPopFront(SLTNode** pphead)
{
  assert(pphead);
  assert(*pphead);
  //以下代码能适用于一个节点和多个节点,如果只有一个节点的情况,先备份NULL,就将NULL赋值于*pphead
  //备份第2个节点的地址
  SLTNode* temp = (*pphead)->next;
  //释放第一个节点
  free(*pphead);
  //再将拷贝的节点链接起来
  (*pphead) = temp; 
}

📐 测试

⭕ 接口7:查找数据 (SListFind)

函数原型:

函数实现:

SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
  //据析,如果是空链表时,这里就无法查找,所以需要断言
  assert(phead);
  SLTNode* cur = phead;
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;//返回x所在节点的地址
    }
    else
    {
      cur = cur->next;
    }
  }
  return NULL;//找不到返回空
}

⭕ 接口8:指定的数之前插入数据 (SlistInser),不支持尾插,详解请看下图

因此可以看出单链表不适合在指定的数的前面插入的,因为它需要前面一个节点的地址

函数原型:

函数实现:

void SlistInser(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
  //据析,这里需要调用SlistFind函数来配合使用,所以这里不需要判断是否为空链表,因为SlistFind函数内已经断言过了
  assert(pphead);
  assert(pos);
  //特殊情况
  //1、调用头插的接口
  if (*pphead == pos)
  {
    SListPushFront(pphead, x);
  }
  //2、非头插入 - 不包含尾插
  else
  {
    //找pos位置的前一个节点
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    SLTNode* newnode = BuySListNode(x);
    //注意这里它们前后链接时可以颠倒
    newnode->next = pos;
    prev->next = newnode;
  }
}

📐 测试

⭕ 接口9:指定的数之后插入数据 (SlistInser),不支持头插,详解请看下图

函数原型:

函数实现:

void SlistInserAfter(SLTNode* pos, SLTDataType x)
{
  //据析,这里也不需要判断空链表的情况
  assert(pos);
  SLTNode* newnode = BuySListNode(x);
  //注意,必须先把pos后面节点的地址交给newnode->next,再将newnode的地址交给pos->next;两者不能颠倒
  newnode->next = pos->next;
  pos->next = newnode;  
}

📐 测试

⭕ 接口10:指定的数删除 (SlistErase),详解请看下图

因此可以看出单链表不适合在删除指定的数,因为它需要前面一个节点的地址

函数原型:

函数实现:

void SlistErase(SLTNode** pphead, SLTNode* pos)
{
  //据析,这里也不需要判断空链表的情况
  assert(pphead);
  //1、调用头删
  if (pos == *pphead)
  {
    SListPopFront(pphead);
  }
  //2、其余节点 - 包括尾删
  else
  {
    //找pos位置的前一个节点
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}

📐 测试

⭕ 接口11:指定的数之后删除 (SlistEraseAfter),详解请看下图

函数原型:

函数实现:

void SlistEraseAfter(SLTNode* pos)
{
  //据析如果那个数后面为NULL,就删除不了,需要断言
  assert(pos);
  assert(pos->next);  
  //拷贝一份
  SLTNode* temp = pos->next->next;
  free(pos->next);
  pos->next = NULL;
  pos->next = temp;
}

📐 测试

⭕ 接口12:统计 (SListSize)

函数原型:

函数实现:

int SListSize(SLTNode* phead)
{
  //这里不需要断言
  int size = 0;
  SLTNode* cur = phead;
  while (cur)
  {
    size++;
    cur = cur->next;
  }
  return size;
}

📐 测试

⭕ 接口13:判空 (SListSize)

函数原型:

函数实现:

bool SListEmpty(SLTNode* phead)
{
  //判空,空是真,非空是假
  //写法1
  //return phead == NULL ? true : false;
  //写法2
  //if (phead == NULL)
  //{
  //  return true;
  //}
  //else
  //{
  //  return false;
  //}
  //写法3
  return phead == NULL; 
}

📐 测试

⭕ 接口14:销毁 (SListDestory)

函数原型:

函数实现:

void SListDestory(SLTNode** pphead)
{
  assert(pphead);
  SLTNode* cur = *pphead;
  while (cur)
  {
    SLTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  *pphead = NULL;
}

📐 测试

📝 完整代码

SList.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int SLTDataType;
typedef struct SListNode
{
  SLTDataType data;//存储整型数据
  struct SListNode* next;//指向下一个节点的地址 
}SLTNode;
//只读的函数接口
void SListPrint(SLTNode* phead);
int SListSize(SLTNode* phead);
bool SListEmpty(SLTNode* phead);
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
SLTNode* BuySListNode(SLTDataType x);
void SListInserAfter(SLTNode* pos, SLTDataType x);
void SListEraseAfter(SLTNode* pos);
void SListDestory(SLTNode** pphead);
//读写的函数接口
void SListPushBack(SLTNode** pphead, SLTDataType x);
void SListPushFront(SLTNode** pphead, SLTDataType x);
void SListPopBack(SLTNode** pphead);
void SListPopFront(SLTNode** pphead);
void SListInser(SLTNode** pphead,  SLTNode* pos, SLTDataType x);
void SListErase(SLTNode** pphead, SLTNode* pos);

SList.c

#include"SList.h"
void SListPrint(SLTNode* phead)
{
  //据情况分析,此处不需要断言,因为我们想在空链表时输出NULL
  //assert(phead);
  SLTNode* cur = phead;
  //遍历
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}
SLTNode* BuySListNode(SLTDataType x)
{
  //每次调用开辟一个节点的空间
  SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
  if (node == NULL)
  {
    printf("malloc fail\n");
    exit(-1);
  }
  node->data = x;
  node->next = NULL;
  //开辟成功,返回地址
  return node;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
  //据析,指针可能为空,但是指针的地址不可能为空,所以需要断言(pphead就是plist的地址),且这里不能断言*pphead,因为这里空链表是可以处理的
  assert(pphead);
  //特殊情况
  //1、空链表
  if (*pphead == NULL)
  {
    //调用BuySListNode去开辟新节点
    SLTNode* newnode = BuySListNode(x);
    *pphead = newnode;
  }
  //2、非空链表
  else
  {
    SLTNode* tail = *pphead;
    //找尾 - NULL
    while(tail->next != NULL)
    {
      tail = tail->next;
    } 
    //开辟节点
    SLTNode* newnode = BuySListNode(x);
    tail->next = newnode; 
  }
}
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
  assert(pphead);
  //对于首插来说,没有特殊情况,以下代码适用于空链表和非空链表
  SLTNode* newnode = BuySListNode(x);
  newnode->next = *pphead;
  *pphead = newnode;
}
void SListPopBack(SLTNode** pphead)
{
  //据析,这里需要断言plist的地址是否传成NULL了;以及没有节点的情况
  assert(pphead);
  assert(*pphead);
  //特殊情况
  //一个节点的情况
  if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    //多个节点的情况 - 找尾 
    //1.先指向第一个节点的位置
    SLTNode* tail = *pphead;
    //2.删尾(错误示范) - 这样删尾会造成野指针(因为每个节点都是一个局部节点,如果这样释放掉后,则前一个节点的next就是一个野指针)
    /*while (tail->next != NULL)
    {
      tail = tail->next;
    }
    free(tail);
    tail = NULL; */
    //2.删尾(正确示范1)
    /*while (tail->next->next != NULL)
    { 
      tail = tail->next; 
    }
    free(tail->next);
    tail->next = NULL;*/
    //2.删尾(正确示范2) - 双指针
    SLTNode* prev = NULL;
    while (tail->next != NULL)
    {
      prev = tail;
      tail = tail->next;
    }
    free(tail->next);
    prev->next = NULL;
  }
}
void SListPopFront(SLTNode** pphead)
{
  assert(pphead);
  assert(*pphead);
  //以下代码能适用于一个节点和多个节点,如果只有一个节点的情况,先备份NULL,就将NULL赋值于*pphead
  //备份第2个节点的地址
  SLTNode* temp = (*pphead)->next;
  //释放第一个节点
  free(*pphead);
  //再将拷贝的节点链接起来
  (*pphead) = temp; 
}
int SListSize(SLTNode* phead)
{
  //这里不需要断言
  int size = 0;
  SLTNode* cur = phead;
  while (cur)
  {
    size++;
    cur = cur->next;
  }
  return size;
}
bool SListEmpty(SLTNode* phead)
{
  //判空,空是真,非空是假
  //写法1
  //return phead == NULL ? true : false;
  //写法2
  //if (phead == NULL)
  //{
  //  return true;
  //}
  //else
  //{
  //  return false;
  //}
  //写法3
  return phead == NULL; 
}
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
  //据析,如果是空链表时,这里就无法查找,所以需要断言
  assert(phead);
  SLTNode* cur = phead;
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;//返回x所在节点的地址
    }
    else
    {
      cur = cur->next;
    }
  }
  return NULL;//找不到返回空
}
void SListInser(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
  //据析,这里需要调用SlistFind函数来配合使用,所以这里不需要判断是否为空链表,因为SlistFind函数内已经断言过了
  assert(pphead);
  assert(pos);
  //特殊情况
  //1、调用头插的接口
  if (*pphead == pos)
  {
    SListPushFront(pphead, x);
  }
  //2、非头插入 - 不包含尾插
  else
  {
    //找pos位置的前一个节点
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    SLTNode* newnode = BuySListNode(x);
    //注意这里它们前后链接时可以颠倒
    newnode->next = pos;
    prev->next = newnode;
  }
}
void SListInserAfter(SLTNode* pos, SLTDataType x)
{
  //据析,这里也不需要判断空链表的情况
  assert(pos);
  SLTNode* newnode = BuySListNode(x);
  //注意,必须先把pos后面节点的地址交给newnode->next,再将newnode的地址交给pos->next;两者不能颠倒
  newnode->next = pos->next;
  pos->next = newnode;  
}
void SListErase(SLTNode** pphead, SLTNode* pos)
{
  //据析,这里也不需要判断空链表的情况
  assert(pphead);
  //1、调用头删
  if (pos == *pphead)
  {
    SListPopFront(pphead);
  }
  //2、其余节点 - 包括尾删
  else
  {
    //找pos位置的前一个节点
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}
void SListEraseAfter(SLTNode* pos)
{
  //据析如果那个数后面为NULL,就删除不了,需要断言
  assert(pos);
  assert(pos->next);  
  //拷贝一份
  SLTNode* temp = pos->next->next;
  free(pos->next);
  pos->next = NULL;
  pos->next = temp;
}
void SListDestory(SLTNode** pphead)
{
  assert(pphead);
  SLTNode* cur = *pphead;
  while (cur)
  {
    SLTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  *pphead = NULL;
}

Test.c

#include"SList.h"
void TestSList1()
{
  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;
  SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
  n4->data = 4;
  n1->next = n2;
  n2->next = n3;
  n3->next = n4;
  n4->next = NULL;
  SLTNode* plist = n1;
  SListPrint(plist);
}
void TestSList2()
{
  //定义plist变量
  SLTNode* plist = NULL;
  //尾插
  SListPushBack(&plist, 1);
  SListPushBack(&plist, 2);
  SListPushBack(&plist, 3);
  SListPushBack(&plist, 4);
  SListPrint(plist);
  //头插
  SListPushFront(&plist, 0);
  SListPushFront(&plist, -1);
  SListPushFront(&plist, -2);
  SListPrint(plist);
  //尾删
  SListPopBack(&plist);
  SListPrint(plist);
  //头删
  SListPopFront(&plist);
  SListPrint(plist);
  //查找 - 查找空链表时,断言报错;查找失败返回NULL;查找成功返回x所在的节点的地址 
  SLTNode* pos = SListFind(plist, 0);
  if (pos)
  {
    printf("找到了\n");
  }
  //指定的数之前插入数据 - 此函数实现不了尾插
  pos = SListFind(plist, -1);
  if (pos)//找到了才插入数据
  {
    SListInser(&plist, pos, -2);
  }
  SListPrint(plist);
  //指定的数之后插入数据 - 此函数实现不了头插
  pos = SListFind(plist, 3);
  if (pos)
  {
    SListInserAfter(pos, 4);
  }
  SListPrint(plist);
  //指定的数删除
  pos = SListFind(plist, 4);
  if (pos)
  {
    SListErase(&plist, pos);
  }
  SListPrint(plist);
  //删除指定的数之后的数
  pos = SListFind(plist, 2);
  if (pos)
  {
    SListEraseAfter(pos);
  }
  SListPrint(plist);
  //统计
  printf("%d\n", SListSize(plist));
  //判空,空是真,非空是假
  printf("%d\n", SListEmpty(plist));
  //销毁
  SListDestory(&plist);
}
int main()
{
  TestSList2();
  return 0;
}

💨 结果

四、顺序表和链表的区别和联系

不同点 顺序表 链表
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问 支持 O(1) 不支持 O(N)
任意位置插入或删除元素 可能需要搬移元素,效率低 只需要修改指针指向
插入 动态顺序表,空间不够时需要扩容 没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁
缓存利用率、缓存命中率

💨 总结:

链表和顺序表没有谁更优,它们各有优缺点,相辅相成

备注:缓存利用率参考存储体系结构以及局部原理性

假设写了一个程序,实现分别对顺序表和链表上的每个数据+1,那么这个程序会编译成指令,然后CPU再执行

CPU运算的速度很快,那内存的速度跟不上,CPU一般就不会直接访问内存,而是把要访问的数据先加载到缓存体系,如果是 ≤ 8byte的数据 (寄存器一般是8byte),会直接到寄存器;而大的数据,会到三级缓存,CPU直接跟缓存交互。

CPU执行指令运算要访问内存,先要取0x10 (假设) 的数据,拿0x10去缓存中找,发现没有 (不命中) ,这时会把主存中这个地址开始的一段空间都读进来 (缓存)。

如果是整型数组,下一次访问0x14、0x18… 就会命中

如果是链表,先取第1个节点0x60,不命中;再取第2个0x90,不命中…。这样会造成一定的缓存污染

相关文章
|
9天前
|
存储
顺序表和链表(2)
【10月更文挑战第23天】
顺序表和链表(2)
|
10天前
|
存储 算法 数据管理
顺序表和链表(1)
【10月更文挑战第22天】
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
22 3
|
5月前
|
存储 缓存 算法
数据结构和算法学习记录——总结顺序表和链表(双向带头循环链表)的优缺点、CPU高速缓存命中率
数据结构和算法学习记录——总结顺序表和链表(双向带头循环链表)的优缺点、CPU高速缓存命中率
47 0
|
1月前
|
存储
一篇文章了解区分指针数组,数组指针,函数指针,链表。
一篇文章了解区分指针数组,数组指针,函数指针,链表。
17 0
|
3月前
|
存储 算法
【初阶数据结构篇】顺序表和链表算法题
此题可以先找到中间节点,然后把后半部分逆置,最近前后两部分一一比对,如果节点的值全部相同,则即为回文。
28 0
|
3月前
|
存储 缓存
【数据结构】——顺序表与链表
【数据结构】——顺序表与链表
|
5月前
|
存储 索引
顺序表和链表
通过以上示例,我们可以看到顺序表和链表在实际应用中如何操作。顺序表适合于需要频繁读取数据的场景,而链表则更适用于频繁修改数据的情况。在选择使用哪种数据结构时,应考虑到实际应用的需求和上下文环境。
32 2
|
5月前
|
存储 SQL 算法
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表
|
5月前
|
存储 SQL 算法
LeetCode 题目 86:分隔链表
LeetCode 题目 86:分隔链表