【数据结构】万字链表详解


前言

1. 链表的概念及结构

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

换句话说,分散存储是链表最显著的特征,为了能够保持数据元素间的顺序关系,在创建每个元素的同时都要配备一个指针,用于指向它的后继元素。这样由指针相互连接的数据就具有了线性的关联。
链表中的数据有俩个部分组成:一是本身存储的数据信息,二是指向后继元素的指针。这两部分组成数据元素的存储结构称为链表的结点,所有的节点通过指针相互链接就组成了一个链表。

链表的特点:

  • 可以按需申请和释放
  • 不存在扩容的问题
  • 插入删除元素的效率高

在这里插入图片描述
注意:

  1. 上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
  2. 现实中的节点一般都是从堆上申请出来的
  3. 申请堆上的空间,可能连续也可能不连续

2. 链表的分类

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

  • 单向或双向
  • 带头或不带头
  • 循环或非循环
  • 无头单向非循环链表
  • 带头双向循环链表

虽然有这么多的链表的结构,但是实际中最常用还是两种结构:
在这里插入图片描述

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

3. 单链表的实现

无头+单向+非循环链表增删查改实现:

//测试文件
#define _CRT_SECURE_NO_WARNINGS
#include "SList.h"
void test2()
{
	SLTNode* pList = NULL;
	//注意修改pList的指向要传它的地址
	LinkListPushFront(&pList, 1);
	PrintLinkList(pList);
	LinkListPushBack(&pList, 7);
	pos = SearchLinkList(pList, 5);
	if (pos)
	{
		InsertLinkList(&pList, pos, 33);
	}
	PrintLinkList(pList);
	DestroyLinkList(&pList);
}
int main()
{
	/*test();
	test1();*/
	test2();
	return 0;
}
//逻辑实现
#include "SList.h"

//创建新结点
//头插尾插都需要新结点
SLTNode* BuyNewNode(SLTypeData elem)
{
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newNode == NULL)
	{
		perror("malloc::");
		exit(-1);
	}
	//创建后把它的成员初始化
	//最后返回newNode的地址
	newNode->val = elem;
	newNode->next = NULL;

	return newNode;
}

//打印链表
//这里不用进行断言
//当链表没有元素时为空是正常情况
void PrintLinkList(SLTNode* phead)
{
	SLTNode* tmp = phead;
	while (tmp)
	{
		printf("%d -> ", tmp->val);
		//打印完后把tmp指向的下一个元素地址赋值给tmp
		tmp = tmp->next;
	}
	puts("NULL");
}

//头插
void LinkListPushFront(SLTNode** pphead, SLTypeData elem)
{
	assert(pphead);
	SLTNode* newNode = BuyNewNode(elem);
	
	//把创建好的新节点的指针赋值为当前的头指针
	//这时newNode->next就指向了头结点
	newNode->next = *pphead;

	//然后把新节点的地址赋值给头指针
	//newNode就成了新的头指针指向第一个结点
	*pphead = newNode;
}

//尾插
void LinkListPushBack(SLTNode** pphead, SLTypeData elem)
{
	assert(pphead);
	SLTNode* newNode = BuyNewNode(elem);
	//两种情况:
	//1. 链表为空
	if (*pphead == NULL)
	{
		//直接调用头插 
		//LinkListPushFront(pphead, elem);	 
		//或者直接赋值
		*pphead = newNode;
	}
	else
	{
		//2. 链表不为空
		//找到最后一个结点								    
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		//让最后一个结点的next指向newNode
		tail->next = newNode;

		//这里改变了指向为什么不用二级指针
		//这要分情况讨论
		//第一种情况如果链表为空
		//要修改的是pList,这是一个结构体指针
		//修改结构体指针就需要二级指针也就是pList的地址

		//第二种情况链表不为空
		//要修改的是tail所指向的结构体成员next
		//修改结构体成员,因此一级结构体指针就够了
	}
}

//尾删
void LinkListPopBack(SLTNode** pphead)
{
	assert(pphead);
	
	//暴力的检查,直接报错
	//assert(*pphead);
	
	//温柔的检查
	//判断是否为空,不为空才有的删
	if(*pphead == NULL)
	{
		puts("无结点可以删除");
		return;
	}

	//如果只有一个结点直接free
	//多个结点才有下面的情况
	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;*/

		//第二种写法
		//双指针一前一后遍历链表
		SLTNode* prev = tail;
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		free(prev->next);
		prev->next = NULL;
	}
}

//头删
void LinkListPopFront(SLTNode** pphead)
{
	assert(pphead);

	//暴力的检查,直接报错
	//assert(*pphead);

	//温柔的检查
	//同样判断是否为空
	if (*pphead)
	{
		SLTNode* tmp = *pphead;
		//先把头指针存起来
		//把头指针赋值为下一个节点的地址
		*pphead = (*pphead)->next;
		//然后释放前一个结点
		free(tmp);
		tmp = NULL;
	}
	else
	{
		puts("无结点可以删除!");
		return;
	}
}

//销毁链表
void DestroyLinkList(SLTNode** pphead)
{
	assert(pphead);

	//快慢指针释放空间
	SLTNode* tmp = *pphead;
	while (tmp)
	{
		SLTNode* cur = tmp;
		tmp = tmp->next;
		free(cur);
	}
	*pphead = NULL;
}

//查找链表
//该函数还可以充当修改删除和插入的作用
SLTNode* SearchLinkList(SLTNode* phead, SLTypeData targrt)
{
	assert(phead);

	SLTNode* tmp = phead;
	while (tmp)
	{
		if (tmp->val == targrt)
		{
			return tmp;
		}
		tmp = tmp->next;
	}
	return NULL;
}

//在pos之前插入结点
void InsertLinkList(SLTNode** pphead, SLTNode* pos, SLTypeData target)
{
	assert(pphead);
	assert(pos);

	SLTNode* newNode = BuyNewNode(target);

	//如果pos等于头节点就相当于头插调用头插即可
	if (pos == *pphead)
	{
		//头插
		//LinkListPushFront(pphead, target);
		newNode->next = *pphead;
		*pphead = newNode;
	}
	else
	{
		//找到pos前一个结点进行插入
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{							
			prev = prev->next;
		}
		newNode->next = pos;
		prev->next = newNode;
	}
}

//在pos之后插入结点
void InsertAfterLinkList(SLTNode* pos, SLTypeData target)
{
	assert(pos);

	SLTNode* newNode = BuyNewNode(target);
	newNode->next = pos->next;
	pos->next = newNode;
}												    

//删除pos位置的结点
void DeleteLinkList(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	
	if (pos == *pphead)
	{
		LinkListPopFront(pphead);
	}
	else
	{
		//找到pos的前一个结点直接让prev->next指向pos->next
		//然后释放pos
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
			//检查是否pos传错了
			assert(prev);
		}
		prev->next = pos->next;
		free(pos);
	}
}

//删除pos位置后面的结点
void DeleteAfterLinkList(SLTNode* pos)
{
	assert(pos);

	if (pos->next != NULL)
	{
		SLTNode* tmp = pos->next;
		pos->next = pos->next->next;
		free(tmp); 
	}
	else
	{
		puts("无元素可以删除");
		return;
	}
}
//头文件包含和函数声明
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

typedef int SLTypeData;

//结构体成员
//一个数据元素
//一个指向下一个结点的指针
typedef struct SLinkListNode
{
	SLTypeData val;
	struct SLinkListNode* next;

}SLTNode;

//创建结点
SLTNode* BuyNewNode(SLTypeData);

//打印链表
void PrintLinkList(SLTNode*);

//头插
void LinkListPushFront(SLTNode**, SLTypeData);

//尾插
void LinkListPushBack(SLTNode**, SLTypeData);

//尾删
void LinkListPopBack(SLTNode**);

//头删
void LinkListPopFront(SLTNode**);

//查找链表
SLTNode* SearchLinkList(SLTNode*, SLTypeData);

//在pos之前插入结点
void InsertLinkList(SLTNode**, SLTNode*, SLTypeData);

//在pos之后插入结点
void InsertAfterLinkList(SLTNode*, SLTypeData);

//删除pos位置的结点
void DeleteLinkList(SLTNode**, SLTNode*);

//销毁链表
void DestroyLinkList(SLTNode**);

写到这里不难发现,单链表只适合头插和头删,其它操作效率都是比较低的。


4. 两道经典链表面试题

4.1 环形链表2

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。
示例:
在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

来源:力扣(LeetCode)
链接:环形链表

有两种解法:

  1. 公式法
  2. 转换法,转换成链表相交

先来看第一种方法:
定义两个指针fast 与 slow。它们起始都位于链表的头部。随后,slow 指针每次向后移动一个位置,而fast 指针向后移动两个位置。如果链表中存在环,则fast 指针最终将再次与slow 指针在环中相遇。

进环后快指针开始追赶慢指针,由于每次之间的距离会缩小1,因此最终快慢指针会在一处相遇。

在这里插入图片描述
有了上面的概念接下来推导公式:
fast走的距离 = 2*slow走的距离,然后假设一些可能的情况。
在这里插入图片描述
根据上面可以得出距离公式为:
2(L+X) = L+X+N*C
进一步化简:
L+X = N*C
L = N*C-X
L = (N-1)*C+C-X
由这个公式可以得出一个结论:一个指针从头开始走,另一个指针从相遇点走,之后会在入口点相遇。
代码实现:

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

方法二,转换成链表相交问题:
找到相遇点,然后再找到相遇点的下一个位置,把相遇点置空后,一个指针从头开始,另一个指针从当前位置开始计算出两个链表的长度,然后让长的链表先走差距步,在同时走,当两个链表相交也就遇到了两个地址相同的结点,返回此结点就是要求的解了。

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode * longA = headA, *shortB = headB;
    int lenA = 0;
    int lenB = 0;
    while(longA->next)
    {
        ++lenA;
        longA = longA->next;
    }

    while(shortB->next)
    {
        ++lenB;
        shortB = shortB->next;
    }
    longA = headA;
    shortB = headB;
    if(lenA < lenB)
    {
        longA = headB;
        shortB = headA;
    }

    int gap = abs(lenB - lenA);
    while(gap--)
    {
        longA = longA->next;
    }
    while(longA != shortB)
    {
        longA = longA->next;
        shortB = shortB->next;
    }
    return longA;
}
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow = head, *fast = head;
    while (fast && slow && fast->next && slow->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (fast == slow) {
            struct ListNode* ptr = fast->next;
            fast->next = NULL;
            struct ListNode* ret = getIntersectionNode(ptr, head);
            fast->next = ptr;
            return ret;
        }
    }
    return NULL;
}

4.2 赋值带随机指针的链表【中等难度】

在这里插入图片描述

来源:力扣(LeetCode)
链接:赋值带随机指针的链表

同样有两种解题思路:

  1. 暴力法
  2. 迭代 + 节点拆分

先来分析暴力法:
1.遍历原链表,复制原链表结点尾插到新链表。
2.遍历原链表中的random指针指向的结点,并把新链表中的random也指向新链表对应的结点即可。

代码实现:

struct Node* copyRandomList(struct Node* head) {
	//创建新链表
	struct Node* newGuard = (struct Node*)malloc(sizeof(struct Node));
    struct Node* tmphead = head, *tmpnewNode = newGuard;
    while(tmphead)
    {
    	//复制原链表到新链表
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->val = tmphead->val;
        newNode->next = tmphead->next;
        newNode->random = tmphead->random;
        tmpnewNode->next = newNode;
        tmpnewNode = tmpnewNode->next;
        
        tmphead = tmphead->next;
    }
    tmpnewNode->next = NULL;
    //同时遍历新链表和原链表中random指向的结点
    //原链表random指向第i个结点
    //对应新链表的random也指向新链表中对应结点
    for(struct Node* tmp = newGuard->next, *tmphead = head; tmp; tmp = tmp->next)
    {
        for(struct Node* tmptar = newGuard->next, *tmpheadtar = head; tmptar; tmptar = tmptar->next)
        {
            if(tmphead->random == tmpheadtar)
            {
                tmp->random = tmptar;
                goto there;
            }
            tmpheadtar = tmpheadtar->next;
        }
        tmp->random = NULL;
        there:
        tmphead = tmphead->next;
    }
    return newGuard->next;
}

这种方法可以解决此问题,但是暴力法的时间复杂度为O(N2),效率是比较低的。

这道题最大的问题在于要去找对应的copy结点,原链表的random指向对应的结点,但是新链表的random怎么找自己的指向的结点呢?原链表的和新链表的结点地址并没有必然的关系,所以就只能用暴力去找相应的对应关系。


根据上面的信息接下来分析第二种方法的思路:
1.拷贝原结点,并链接在原结点的后面。

原结点与拷贝结点建立链接关系,找到原结点就可以找到拷贝结点

在这里插入图片描述
2.更新每个拷贝节点的random,因为每个拷贝结点都在原结点的下一个结点,因此如果原链表的random不为空,那么copy->random = head->random->next,找到了head的random指向的结点,它的下一个结点就是copy的random指向的结点,如果为空对应的copy的random也为空。

妙!太妙了!遍历原链表的random也就可以更新新链表的random。

在这里插入图片描述
3.copy结点更新结束后,应该取下copy结点,链接为新链表,并把原结点的指向恢复为链接copy结点前相应的指向关系。

代码实现:

struct Node* copyRandomList(struct Node* head) {
    if(!head)
        return NULL;
    struct Node* tmphead = head;
    struct Node* newNode = NULL;
    while(tmphead)
    {
        //链接拷贝结点到原结点的后面
        struct Node* tmpheadNext = tmphead->next;
        newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->val = tmphead->val;

        tmphead->next = newNode;
        newNode->next = tmpheadNext;
        //迭代走
        tmphead = tmpheadNext;
    }
    //遍历原链表的random更新拷贝结点的random
    tmphead = head;
    while(tmphead)
    {
        newNode = tmphead->next;
        if(tmphead->random)
        {
            newNode->random = tmphead->random->next;
        }
        else
        {
            newNode->random = NULL;
        }
        
        tmphead = newNode->next;
    }
    //把copy链表截取下来,恢复原链表
    tmphead = head;
    //定义哨兵结点,把copy结点依次拿下来尾插
    struct Node* retList = (struct Node*)malloc(sizeof(struct Node));
    struct Node* tmpList = retList;
    while(tmphead)
    {
        //尾插
        tmpList->next = tmphead->next;
        tmpList = tmpList->next;
        //迭代
        tmphead = tmpList->next;
    }
    return retList->next;
}

5. 带头双向循环链表的实现

链表的分类有很多,但是用的最多的就是单链表和带头双向循环链表,而此链表是最为复杂的结构,因此本篇只介绍这两种,对于其它类型的链表,在掌握了带头双向循环链表就很容易理解了。

在这里插入图片描述
这里的head结点也称为哨兵结点,它通常不存储有效数据,是可以用来简化边界条件。是一个附加的链表节点,该节点作为第一个节点,只是为了操作的方便而设置。
如果一个链表有哨兵节点,那么该链表的第一个元素应该是链表的第二个节点。

哨兵结点有两个指针,一个指针指向尾结点,另一个指针指向它的后继结点。并且之后的结点都配备两个指针,可以指向自己的前一个和后一个结点,而最后一个结点的一个指针可以指针头节点。因此称为带头双向循环链表。

该链表结构虽然复杂,但是实现比较简单,接下来开始实现。

//头文件函数声明
#pragma once 
#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>


typedef int DATAType;
typedef struct DoubleListNode
{
	struct DoubleListNode* prev;
	struct DoubleListNode* next;
	DATAType data;
}DLNode;

//初始化
DLNode* InitDList();

//打印链表
void PrintList(DLNode* phead);

//销毁链表
void Destroy(DLNode* pHead);

//头插
void ListPushFront(DLNode* phead, DATAType data);

//头删
void ListPopBack(DLNode* phead);

//尾插
void ListPushBack(DLNode* phead, DATAType data);

//尾删
void ListPopFront(DLNode* phead);

//判空
bool isListEmpty(DLNode* phead);

//链表结点个数
size_t ListNum(DLNode* phead);

//查找链表
DLNode* FineNode(DLNode* phead, DATAType target);

//在目标位置前插入结点
void InsertPosNode(DLNode* pos, DATAType data);

//删除目标位置结点
void DeletePosNode(DLNode* pos);
#define _CRT_SECURE_NO_WARNINGS 1
#include "dList.h"
//函数实现

//初始化
DLNode* InitDList()
{
	DLNode* guard = (DLNode*)malloc(sizeof(DLNode));
	if (!guard)
	{
		perror("malloc fail");
		exit(-1);
	}

	guard->next = guard;
	guard->prev = guard;
												    
	return guard;
}
//打印链表
void PrintList(DLNode* phead)
{
	assert(phead);
	DLNode* tmpHead = phead->next;
	printf("Guard <-> ");
	while (tmpHead != phead)
	{
		printf("%d <-> ", tmpHead->data);
		tmpHead = tmpHead->next;
	}
	puts("Guard");
}

//创建结点
DLNode* BuyNode(DATAType data)
{
	DLNode* newNode = (DLNode*)malloc(sizeof(DLNode));
	if (!newNode)
	{
		perror("newNode malloc fail");
		exit(-1);
	}
	newNode->next = NULL;
	newNode->prev = NULL;
	newNode->data = data;

	return newNode;
}

//尾插
void ListPushBack(DLNode* phead, DATAType data)
{
	assert(phead);
	DLNode* newNode = BuyNode(data);
	/*DLNode* tail = phead->prev;
	
	tail->next = newNode;
	newNode->prev = tail;

	newNode->next = phead;
	phead->prev = newNode;  */

	//把头传过去就是尾插
	InsertPosNode(phead, data);
}

//头插
void ListPushFront(DLNode* phead, DATAType data)
{
	assert(phead);
	DLNode* newNode = BuyNode(data);

	//先链接phead->next和newNode的关系
	/*newNode->next = phead->next;
	phead->next->prev = newNode;

	phead->next = newNode;
	newNode->prev = phead;*/

	//不关心顺序
	/*DLNode* next = phead->next;
	phead->next = newNode;
	newNode->prev = phead;
	newNode->next = next;
	next->prev = newNode;*/

	//哨兵结点的下一个位置就是头插
	InsertPosNode(phead->next, data);
}
	
//判空
bool isListEmpty(DLNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

//尾删
void ListPopBack(DLNode* phead)
{
	assert(phead);
	assert(!isListEmpty(phead));

	/*DLNode* delTail = phead->prev;
	DLNode* prev = delTail->prev;
	prev->next = phead;
	phead->prev = prev;

	free(delTail);*/

	DeletePosNode(phead->prev);
}

//头删
void ListPopFront(DLNode* phead)
{
	assert(phead);
	assert(!isListEmpty(phead));

	/*DLNode* delNode = phead->next;
	phead->next = delNode->next;
	delNode->next->prev = phead;
	free(delNode);*/

	DeletePosNode(phead->prev);
}

//链表结点个数
size_t ListNum(DLNode* phead)
{
	assert(phead);
	size_t sum = 0;
	DLNode* tmpHead = phead->next;
	while (tmpHead != phead)
	{
		++sum;
		tmpHead = tmpHead->next;
	}
	
	return sum;
}

//查找链表
DLNode* FineNode(DLNode* phead, DATAType target)
{
	assert(phead);

	DLNode* tmpHead = phead->next;
	while (tmpHead != phead)
	{
		if (tmpHead->data == target)
		{
			return tmpHead;
		}
		tmpHead = tmpHead->next;
	}
	return NULL;
}

//在目标位置前插入结点
void InsertPosNode(DLNode* pos, DATAType data)
{
	assert(pos);
	DLNode* newNode = BuyNode(data);

	DLNode* tmpPosPrev = pos->prev;
	pos->prev = newNode;
	newNode->next = pos;
	tmpPosPrev->next = newNode;
	newNode->prev = tmpPosPrev;
}
//有了插入就不需要尾插头插了

//删除目标位置结点
void DeletePosNode(DLNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

//销毁链表
void Destroy(DLNode* phead)
{
	assert(phead);
	DLNode* tmpHead = phead->next;
	while (tmpHead != phead)
	{
		DLNode* next = tmpHead->next;
		free(tmpHead);
		tmpHead = next;
	}
	free(phead);
}

6. 顺序表和链表的区别

顺序表的优点:

  1. 尾插尾删效率很高
  2. 支持下标随机访问,这是链表无法比拟的。
  3. 相比链表,顺序表cpu高速缓存利用率更高

缺点:

  1. 扩容问题,如果是异地扩容会造成一定的性能消耗
  2. 扩容会存在一定的空间浪费

链表的优点:

  1. 任意位置插入和删除效率很高
  2. 按需申请和释放

缺点:

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

在这里插入图片描述

个人理解:由于顺序表在内存中的地址是连续的,而链表的地址却并不连续,因此当缓存从内存中读取一段连续的字节数据后,cpu会直接访问缓存中加载的数据,因此如果是顺序表,缓存中的一段字节数据都是有效的顺序表数据,执行起来比较高效,但是链表的地址不连续,所以缓存会不断的去内存中加载链表相应的数据,所以相比而言顺序表cpu高速缓存利用率更高。


本篇完

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值