链表-数据结构和算法之美学习笔记

链表的底层存储结构

​ 数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题

img

单链表

链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫作后继指针 next。

img

其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。

与数组一样,链表也支持数据的查找、插入和删除操作

在进行数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是 O(n)。而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以,在链表中插入和删除一个数据是非常快速的

从图中我们可以看出,针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)

img

弊端链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度

链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。


循环链表:一种特殊的单链表

它跟单链表唯一的区别就在尾结点,单链表的尾结点指针指向空地址,而循环链表的尾结点指针是指向链表的头结点

img

和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题

双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

img

双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。

相比单链表,双向链表适合解决这些问题:

  1. O(1) 时间复杂度的情况下找到前驱结点
  2. 删除给定指针指向的结点时直接得出前驱节点,而不需要遍历一次寻找到当前节点的前驱是谁
  3. 插入给定指针指向的结点时做到O(1)复杂度,单链表需要O(n),道理同上
  4. 对于有序链表,的查询的效率比单链表高。因为,可以记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,平均只需要查找一半的数据

Java中的LinkedHashMap运用到了双向链表

双链表插入操作

假设将p所指结点后插入一个s节点,要首先将s的后继节点作位原先p节点后继的前驱,其中第四行代码必须最后实现,不然就找不到p的后继节点。

s->next = p->next;
p->next->prior= s;
s->prior = p;
p->next = s;
image-20201218174325087

用空间换时间

当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路

缓存实际上就是利用了空间换时间的设计思想。如果我们把数据存储在硬盘上,会比较节省内存,但每次查找数据都要询问一次硬盘,会比较慢。但如果我们通过缓存技术,事先将数据加载在内存中,虽然会比较耗费内存空间,但是每次数据查询的速度就大大提高了

对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化;而消耗过多内存的程序,可以通过消耗更多的时间(时间换空间)来降低内存的消耗

双向循环链表

img

链表和数组性能的对比

img

数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读

数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,看作是二者最大的区别。

指针

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量

p->next=q    这行代码是说,p 结点中的 next 指针存储了 q 结点的内存地址

p->next=p->next->next    这行代码表示,p 结点的 next 指针存储了 p 结点的下下一个结点的内存地址

插入结点时,一定要注意操作的顺序,要先将结点 x 的 next 指针指向结点 b,再把结点 a 的 next 指针指向结点 x,这样才不会丢失指针,导致内存泄漏

img
x->next = p->next;  // 将x的结点的next指针指向b结点;

p->next = x;  // 将p的next指针指向x结点;

删除链表结点时,也一定要记得手动释放内存空间,否则,也会出现内存泄漏的问题

添加“哨兵”简化代码难度

new_node->next = p->next;
p->next = new_node;

当向一个空链表中插入第一个结点,需要进行下面这样的特殊处理,其中 head 表示链表的头结点。所以,从这段代码可以发现,对于单链表的插入操作,第一个结点和其他结点的插入逻辑是不一样的。

if (head == null) {
  head = new_node;
}

同样,对于删除链表中的最后一个结点,需要做如下特殊处理

if (head->next == null) {
   head = null;
}

但如果要删除结点 p(非尾结点) 的后继结点,我们只需要一行代码就可以搞定

p->next = p->next->next;

我们可以看出,针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。有一说一有点鸹貔

带头链表

为了解决鸹貔问题,就引入了哨兵结点,在任何时候,不管链表是不是空,head 指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表。

img

注意,哨兵结点是不存储数据的,这样一来插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑。利用哨兵简化编程难度的技巧,在很多代码实现中都有用到,比如插入排序、归并排序、动态规划等

检查链表代码是否正确的边界条件

  1. 如果链表为空时,代码是否能正常工作?
  2. 如果链表只包含一个结点时,代码是否能正常工作?
  3. 如果链表只包含两个结点时,代码是否能正常工作?
  4. 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

顺序表示例代码

#include "stdio.h"
#include "windows.h"
#include "stdlib.h"

#define MAXSIZE 20//顺序表最大长度

/*定义顺序表*/
typedef struct {
	int data[MAXSIZE];
	int length;
}SeqList;

void InitList(SeqList *l)
{
	l->length = 0;
}

/*建立顺序表*/
int CreatList(SeqList *l, int a[], int n) {
	if (n > MAXSIZE)
	{
		printf("空间不够,无法建立顺序表。\n");
		return 0;
	}
	for (int k = 0; k < n; k++)
	{
		l->data[k] = a[k];
	}
	l->length = n;
	return 1;
}
/*判空操作*/
int Empty(SeqList *l)
{
	if (l->length == 0)
		return 1;
	else
		return 0;
}

/*求顺序表长度*/
int Length(SeqList *l)
{
	return l->length;
}

/*遍历操作*/
void PrintList(SeqList *l)
{
	for (int i = 0; i < l->length; i++)
		printf("%d ", (l->data[i]));
}

/*按值查找*/
int Locate(SeqList *l,int x)
{
	for (int i = 0; i < l->length; i++)
	{
		if (l->data[i] == x)
		{
			return i + 1;
		}
		return 0;

	}
	return 1;
}

/*按位查找*/
int Get(SeqList *l, int x,int *ptr)
{//若查找成功,则通过指针参数ptr返回值
	if ( x <1 || x>l->length){
		printf("查找位置非法,查找错误\n");
		return 0;
	}
	else
	{
		*ptr = l->data[x];
		return 1;
	}
}

/*插入操作*/
int Insert(SeqList *l, int i, int x)
{
	if (l->length > MAXSIZE)
	{
		printf("上溢错误!");
		return 0;
	}
	if (i<1 || i>l->length)
	{
		printf("插入位置错误!");
		return 0;
	}
	for (int k = l->length; k > i; k--)
	{
		l->data[k] = l->data[k - 1];
	}
	l->data[i] = x;
	l->length++;
	return 1;
}

/*删除操作*/
int Delete(SeqList *l, int i, int *ptr)
{
	if (l->length == 0)
	{
		printf("发生下溢错误,即将要访问顺序表之前的地址.\n");
		return 0;
	}
	if (i > l->length || i < 1)
	{
		printf("删除位置错误!\n");
		return 0;
	}
	*ptr = l->data[i - 1];//把要删除的数据返回
	for (int j = i; j < l->length; j++)
	{
		l->data[j - 1] = l->data[j];
	}
	l->length--;
	return 1;
}

/*修改操作*/
int Modify(SeqList *l, int i, int x)
{
	if (i > l->length || i < 1)
	{
		printf("位置错误!\n");
		return 0;
	}
	l->data[i] = x;
	return 1;
}

int main()
{
	int a[5] = {1,2,3,4,5};
	int  i, x;
	SeqList list1;
	InitList(&list1);//初始化顺序表
	if (Empty(&list1))
	{
		printf("初始化顺序表成功!\n");
	}
	printf("给顺序表赋值:1 2 3 4 5\n遍历并输出顺序表:\n");
	CreatList(&list1,a,5 );//建立一个长度为5的线性表
	PrintList(&list1);//遍历输出此顺序表
	printf("\n在第三位后插入一个100:\n");
	Insert(&list1, 3, 100);
	PrintList(&list1);
	if (Modify(&list1, 3, 50) == 1) {
		printf("\n把第三位改成50\n");
		PrintList(&list1);
	}
	if (Delete(&list1, 4, &x) == 1) {
		printf("\n把第四位删除,删除的值是%d\n",x);
		PrintList(&list1);
	}
	system("pause");
	return 0; 
}


单链表示例代码

#include "stdio.h"
#include "windows.h"
#include "stdlib.h"

typedef int ElemType;

typedef struct lnode
{
    ElemType data;
    struct lnode *next;
} LinkNode;

//均采用带头节点的单链表

//头插法创建单链表,l是头指针
void CreateListF(LinkNode *&l, ElemType a[], int n)
{
    LinkNode *s;
    l = (LinkNode *)malloc(sizeof(LinkNode));
    l->next = NULL;
    for (int i = 0; i < n; i++)
    {
        s = l;
        s->data = a[i];
        s->next = l->next;
        l->next = s;
    }
}
//尾插法创建单链表,r是尾指针
void CreateListR(LinkNode *&l, ElemType a[], int n)
{
    LinkNode *s, *r;
    l = (LinkNode *)malloc(sizeof(LinkNode));
    r = l;
    for (int i = 0; i < n; i++)
    {
        s = (LinkNode *)malloc(sizeof(LinkNode));
        s->data = a[i];
        r->next = s;
        r = s;
    }
    r->next = NULL;
}

//初始化线性表
void InitList(LinkNode *&l)
{
    l = (LinkNode *)malloc(sizeof(LinkNode));
    l->next = NULL;
}

//销毁线性表
void DestroyList(LinkNode *&l)
{
    LinkNode *pre = l, *p = l->next; //pre指向p的前驱结点
    while (p != NULL)
    {
        free(pre);
        pre = p;
        p = pre->next;
    }
    free(pre);
}

//判断是否为空表
bool ListEmpty(LinkNode *l)
{
    return (l->next == NULL);
}

//求线性表长度
int ListLength(LinkNode *L)
{
    int n = 0;
    LinkNode *p = L;
    while (p != NULL)
    {
        n++;
        p = p->next;
    }

    return n;
}

//输出线性表
void DispList(LinkNode *L)
{
    LinkNode *p = L->next;
    while (p != NULL)
    {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

//求某个元素元素值
bool GetElem(LinkNode *L, int i, ElemType &e)
{
    int j = 0;
    LinkNode *p = L;
    if (i <= 0)
        return false;
    while (j < i && p != NULL)
    {
        j++;
        p = p->next;
    }
    if (p == NULL)
    {
        return false;
    }
    else
    {
        e = p->data;
        return true;
    }
}

//按照元素值查找节点
int LocateElem(LinkNode *L, ElemType e)
{
    int i = 1;
    LinkNode *p = L->next;
    while (p != NULL && p->data != e)
    {
        p = p->next;
        i++;
    }
    if (p == NULL)
        return 0;
    else
        return i;
}

//插入数据元素
bool ListInsert(LinkNode *&L, int i, ElemType e)
{
    int j = 0;
    LinkNode *p = L, *s;
    if (i <= 0)
        return false;
    while (j < i - 1 && p != NULL)
    {
        j++;
        p = p->next;
    }
    if (p == NULL)
    {
        return false;
    }
    else
    {
        s = (LinkNode *)malloc(sizeof(LinkNode));
        s->data = e;
        s->next = p->next;
        p->next = s;
        return true;
    }
}

//删除数据元素
bool ListDelete(LinkNode *&L, int i, ElemType &e)
{
    int j = 0;
    LinkNode *p = L, *q;
    if (i <= 0)
        return false;
    while (j < i - 1 && p != NULL)
    {
        j++;
        p = p->next;
    }
    if (p == NULL)
    {
        return false;
    }
    else
    {
        q = p->next;
        if (q == NULL)
        {
            return false;
        }
        e = q->data;
        p->next = q->next;
        free(q);
        return true;
    }
}

int main()
{
    LinkNode *L;
    ElemType tmp;

    printf("初始化链表\n");
    InitList(L);

    printf("判断是否为空\n");
    if (ListEmpty(L))
    {
        printf("链表为空!\n\n");
    }
    else
    {
        printf("链表非空\n\n");
    }

    //利用a数组创建链表
    int a[] = {1, 2, 3, 4, 5};
    int lenA = sizeof(a) / sizeof(a[0]);
    printf("利用a数组创建链表\n");
    CreateListR(L, a, lenA);
    printf("\n");

    printf("判断创建后的链表是否为空\n");
    if (ListEmpty(L))
    {
        printf("该链表为空\n\n");
    }
    else
    {
        printf("该链表非空\n\n");
    }

    printf("现有链表:");
    DispList(L);
    printf("\n");

    printf("现有链表长度:%d\n\n", ListLength(L));

    GetElem(L, 2, tmp);
    printf("第二个位置上的元素是:%d\n", tmp);
    printf("\n");

    printf("获取元素值为3的元素在链表中的位置:\n");
    printf("元素值为3的元素在链表中的位置是:%d\n\n", LocateElem(L, 3));

    printf("向第1个位置插入元素246:\n");
    ListInsert(L, 2, 99);
    printf("插入成功!\n\n");

    printf("输出插入元素后的链表:\n");
    DispList(L);
    printf("\n");

    printf("删除第二个位置的元素:\n");
    ListDelete(L, 2, tmp);
    printf("successful!\n\n");

    printf("删除元素后的链表:\n");
    DispList(L);
    printf("\n");

    printf("销毁链表\n");
    DestroyList(L);
    printf("销毁成功\n");
    return 0;
}

双向链表示例代码

#include <stdio.h>
#include <malloc.h>
typedef int ElemType;
typedef struct DNode
{
    ElemType data;
    struct DNode *prior; //指向前驱结点
    struct DNode *next;  //指向后继结点
} DLinkNode;

//声明双链表结点类型
void CreateListF(DLinkNode *&L, ElemType a[], int n) //头插法建双链表
{
    DLinkNode *s;
    L = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
    L->prior = L->next = NULL;
    for (int i = 0; i < n; i++)
    {
        s = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建新结点
        s->data = a[i];
        s->next = L->next; //将结点s插在原开始结点之前,头结点之后
        if (L->next != NULL)
            L->next->prior = s;
        L->next = s;
        s->prior = L;
    }
}

//尾插法建双链表
void CreateListR(DLinkNode *&L, ElemType a[], int n)
{
    DLinkNode *s, *r;
    L = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
    r = L;                                      //r始终指向终端结点,开始时指向头结点
    for (int i = 0; i < n; i++)
    {
        s = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建新结点
        s->data = a[i];
        r->next = s;
        s->prior = r; //将结点s插入结点r之后
        r = s;
    }
    r->next = NULL; //尾结点next域置为NULL
}

//初始化线性表
void InitList(DLinkNode *&L)
{
    L = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
    L->prior = L->next = NULL;
}

//销毁线性表
void DestroyList(DLinkNode *&L)
{
    DLinkNode *pre = L, *p = pre->next;
    while (p != NULL)
    {
        free(pre);
        pre = p; //pre、p同步后移一个结点
        p = pre->next;
    }
    free(p);
}

//判线性表是否为空表
bool ListEmpty(DLinkNode *L)
{
    return (L->next == NULL);
}

//求线性表的长度
int ListLength(DLinkNode *L)
{
    DLinkNode *p = L;
    int i = 0;              //p指向头结点,i设置为0
    while (p->next != NULL) //找尾结点p
    {
        i++; //i对应结点p的序号
        p = p->next;
    }
    return (i);
}

//输出线性表
void DispList(DLinkNode *L)
{
    DLinkNode *p = L->next;
    while (p)
    {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

//求线性表中第i个元素值
bool GetElem(DLinkNode *L, int i, ElemType &e)
{
    int j = 0;
    DLinkNode *p = L;
    if (i <= 0)
        return false;          //i错误返回假
    while (j < i && p != NULL) //查找第i个结点p
    {
        j++;
        p = p->next;
    }
    if (p == NULL) //没有找到返回假
        return false;
    else //找到了提取值并返回真
    {
        e = p->data;
        return true;
    }
}

//查找第一个值域为e的元素序号
int LocateElem(DLinkNode *L, ElemType e)
{
    int i = 1;
    DLinkNode *p = L->next;
    while (p != NULL && p->data != e) //查找第一个值域为e的结点p
    {
        i++; //i对应结点p的序号
        p = p->next;
    }
    if (p == NULL) //没有找到返回0
        return (0);
    else //找到了返回其序号
        return (i);
}

//插入第i个元素
bool ListInsert(DLinkNode *&L, int i, ElemType e)
{
    int j = 0;
    DLinkNode *p = L, *s; //p指向头结点,j设置为0
    if (i <= 0)
        return false;              //i错误返回假
    while (j < i - 1 && p != NULL) //查找第i-1个结点p
    {
        j++;
        p = p->next;
    }
    if (p == NULL) //未找到第i-1个结点
        return false;
    else //找到第i-1个结点p
    {
        s = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建新结点s
        s->data = e;
        s->next = p->next; //将结点s插入到结点p之后
        if (p->next != NULL)
            p->next->prior = s;
        s->prior = p;
        p->next = s;
        return true;
    }
}

//删除第i个元素
bool ListDelete(DLinkNode *&L, int i, ElemType &e)
{
    int j = 0;
    DLinkNode *p = L, *q; //p指向头结点,j设置为0
    if (i <= 0)
        return false;              //i错误返回假
    while (j < i - 1 && p != NULL) //查找第i-1个结点p
    {
        j++;
        p = p->next;
    }
    if (p == NULL) //未找到第i-1个结点
        return false;
    else //找到第i-1个节p
    {
        q = p->next;   //q指向第i个结点
        if (q == NULL) //当不存在第i个结点时返回false
            return false;
        e = q->data;
        p->next = q->next;   //从双链表中删除结点q
        if (p->next != NULL) //若p结点存在后继结点,修改其前驱指针
            p->next->prior = p;
        free(q); //释放q结点
        return true;
    }
}

int main()
{
    DLinkNode *L;
    ElemType tmp;

    printf("初始化链表\n");
    InitList(L);

    printf("判断是否为空\n");
    if (ListEmpty(L))
    {
        printf("链表为空!\n\n");
    }
    else
    {
        printf("链表非空\n\n");
    }

    //利用a数组创建链表
    int a[] = {11, 22, 33, 44, 55};
    int lenA = sizeof(a) / sizeof(a[0]);
    printf("利用a数组创建链表,数组长度为:%d\n", lenA);
    CreateListR(L, a, lenA);
    printf("\n");

    printf("判断创建后的链表是否为空\n");
    if (ListEmpty(L))
    {
        printf("该链表为空\n\n");
    }
    else
    {
        printf("该链表非空\n\n");
    }

    printf("现有链表:");
    DispList(L);
    printf("\n");

    printf("现有链表长度:%d\n\n", ListLength(L));

    GetElem(L, 2, tmp);
    printf("第二个位置上的元素是:%d\n", tmp);
    printf("\n");

    printf("获取元素值为3的元素在链表中的位置:\n");
    printf("元素值为3的元素在链表中的位置是:%d\n\n", LocateElem(L, 3));

    printf("向第1个位置插入元素99:\n");
    ListInsert(L, 2, 99);
    printf("插入成功!\n\n");

    printf("输出插入元素后的链表:\n");
    DispList(L);

    printf("删除第二个位置的元素:\n");
    ListDelete(L, 2, tmp);
    printf("successful!\n\n");

    printf("删除元素后的链表:\n");
    DispList(L);
    printf("\n");

    printf("销毁链表\n");
    DestroyList(L);
    printf("销毁成功\n");
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值