一、线性表的类型定义
-
数据元素类型 :
线性表由一系列数据元素组成,这些数据元素可以是基本数据类型(如整型、浮点型、字符型等),也可以是复杂的数据类型(如结构体、类、指针等)。
-
存储结构 :
线性表的存储结构可以是顺序存储或链式存储。
- 顺序存储:使用连续的存储空间来存储线性表的元素,通常通过数组来实现。
- 链式存储:使用非连续的存储空间来存储线性表的元素,每个元素(称为节点)包含数据域和指针域(或链接域),指针域用于指向下一个节点。
-
基本操作 :
线性表的基本操作包括初始化、插入、删除、查找、遍历等。
- 初始化:创建一个空的线性表。
- 插入:在指定位置插入一个新元素。
- 删除:删除指定位置的元素。
- 查找:查找指定元素的位置或值。
- 遍历:按顺序访问线性表中的每个元素。
-
类型定义方式 :
线性表的类型定义可以使用多种方式,如使用结构体(struct)在C语言中定义,或使用类(class)在C++、Java等面向对象语言中定义。
实例:
cpp
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 // 定义线性表的最大长度
typedef struct {
int data[MAXSIZE]; // 存储线性表元素的数组
int length; // 线性表的当前长度
} SqList;
// 初始化
void InitList(SqList *L) {
L->length = 0;
}
// 存取
int GetElem(SqList L, int i, int *e) {
if (i < 1 || i > L.length) return 0; // 非法位置
*e = L.data[i - 1];
return 1;
}
// 插入
int ListInsert(SqList *L, int i, int e) {
if (i < 1 || i > L->length + 1) return 0; // 非法位置
if (L->length == MAXSIZE) return 0; // 表满
for (int k = L->length - 1; k >= i - 1; k--) {
L->data[k + 1] = L->data[k];
}
L->data[i - 1] = e;
L->length++;
return 1;
}
// 删除
int ListDelete(SqList *L, int i, int *e) {
if (i < 1 || i > L->length) return 0; // 非法位置
*e = L->data[i - 1];
for (int k = i; k < L->length; k++) {
L->data[k - 1] = L->data[k];
}
L->length--;
return 1;
}
// 检索(查找)
int LocateElem(SqList L, int e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e) return i + 1;
}
return 0;
}
// 分解和排序(略)
int main() {
SqList L;
InitList(&L);
// 插入元素
ListInsert(&L, 1, 10);
ListInsert(&L, 2, 20);
ListInsert(&L, 3, 30);
// 存取元素
int e;
GetElem(L, 2, &e);
printf("Element at position 2: %d\n", e);
// 删除元素
ListDelete(&L, 2, &e);
printf("Deleted element: %d\n", e);
// 检索元素
int pos = LocateElem(L, 30);
if (pos) {
printf("Element 30 found at position: %d\n", pos);
} else {
printf("Element 30 not found.\n");
}
return 0;
}
java
class ListNode {
int data;
ListNode next;
ListNode(int data) {
this.data = data;
this.next = null;
}
}
class LinkedList {
ListNode head;
int length;
// 初始化
public LinkedList() {
this.head = null;
this.length = 0;
}
// 存取(这里只展示查找并打印第一个元素,存取特定位置元素需要遍历)
public void printFirstElem() {
if (head != null) {
System.out.println("First element: " + head.data);
} else {
System.out.println("List is empty.");
}
}
// 插入(在链表头部插入)
public void insert(int data) {
ListNode newNode = new ListNode(data);
newNode.next = head;
head = newNode;
length++;
}
// 删除(删除第一个匹配的元素)
public boolean delete(int data) {
if (head == null) return false;
if (head.data == data) {
head = head.next;
length--;
return true;
}
ListNode current = head;
while (current.next != null && current.next.data != data) {
current = current.next;
}
if (current.next != null) {
current.next = current.next.next;
length--;
return true;
}
return false;
}
// 检索(查找元素并返回其位置,这里简化为返回是否存在)
public boolean contains(int data) {
ListNode current = head;
while (current != null) {
if (current.data == data) return true;
current = current.next;
}
return false;
}
// 分解和排序(略)
public static void main(String[] args) {
LinkedList list = new LinkedList();
// 插入元素
list.insert(10);
list.insert(20);
list.insert(30);
// 存取元素(打印第一个元素)
list.printFirstElem();
// 删除元素
list.delete(20);
// 尝试存取元素(打印第一个元素)
list.printFirstElem();
// 检索元素
if (list.contains(30)) {
System.out.println("Element 30 found.");
} else {
System.out.println("Element 30 not found.");
}
}
}
二、线性表的顺序表示和实现
2.1线性表的顺序表示
线性表的顺序存储,又称为顺序表,是一种使用连续存储单元来存储线性表数据元素的方式。在这种存储方式中,数据元素之间的逻辑关系和物理关系是一致的,即它们在内存中的存储顺序与它们在线性表中的顺序相同。
2.1.1 特点
-
元素连续存储:顺序表的元素在内存中占据一块连续的存储空间,这使得通过下标访问元素变得非常高效,时间复杂度为O(1)。
-
随机访问:由于元素是连续存储的,因此可以像访问数组一样,通过下标直接访问顺序表中的任意元素。
-
存储密度高:顺序表不需要为元素之间的逻辑关系而增加额外的存储空间,因此存储密度较高。
-
插入和删除操作可能低效:在顺序表中插入或删除元素时,可能需要移动大量的元素,以保持元素的连续性。特别是在表的中间或靠近末尾的位置进行插入或删除操作时,效率较低。
2.1.2基本操作
-
初始化:为顺序表分配一块足够大的连续存储空间,并设置表的初始状态(如长度为0)。
-
插入:在顺序表的指定位置插入一个新的元素。如果插入位置合法且表未满,则将该位置及其后的元素依次后移一位,然后将新元素插入到指定位置。
-
删除:从顺序表中删除指定位置的元素。如果删除位置合法且表不为空,则将该位置后的元素依次前移一位,然后更新表的长度。
-
查找:根据给定的元素值,在顺序表中查找该元素的位置。如果找到,则返回其位置(下标);否则,返回一个特殊值(如-1)表示未找到。
-
更新:根据给定的位置和元素值,更新顺序表中指定位置的元素。
-
遍历:依次访问顺序表中的每个元素,并对它们进行某种处理(如打印、计算等)。
2.1.3 实现
顺序表通常使用数组来实现。在C语言中,可以使用静态数组或动态数组(如通过malloc
和realloc
函数分配的数组)来存储顺序表的元素。在C++中,可以使用std::vector
等容器类来实现顺序表。
2.1.4 注意事项
-
预分配空间:在使用顺序表之前,通常需要预分配一块足够大的连续存储空间。如果实际存储的元素数量超过了预分配的空间大小,就会发生存储溢出错误。
-
动态扩展:为了处理存储溢出问题,可以使用动态扩展技术。当顺序表的空间不足时,可以重新分配一块更大的连续存储空间,并将原有的元素复制到新的存储空间中。然而,这种操作可能会导致性能下降和内存碎片问题。
-
边界检查:在进行插入、删除和查找等操作时,需要进行边界检查以确保操作的合法性。例如,插入位置应该在表的长度加1的范围内;删除位置应该在表的长度范围内;查找的元素值应该在表的元素范围内等。
2.2 顺序表中基本操作的实现
2.2.1 顺序表初始化
初始化顺序表通常涉及为数据元素分配一块连续的存储空间,并设置表的初始状态(如长度和容量)。
cpp
#define MAX_SIZE 100 // 顺序表的最大容量
typedef struct {
ElemType data[MAX_SIZE]; // 存储元素的数组
int length; // 顺序表的当前长度
} SqList;
// 初始化顺序表
void InitList(SqList *L) {
L->length = 0; // 设置顺序表长度为0
}
2.2.2 顺序表取值
取值操作通过给定的位置(下标)来获取顺序表中的元素。
cpp
// 获取顺序表中第i个位置的元素(1 ≤ i ≤ L->length)
ElemType GetElem(SqList L, int i) {
if (i < 1 || i > L.length) {
// 下标不合法,可以进行错误处理,如返回特殊值或抛出异常
return ERROR_VALUE;
}
return L.data[i - 1]; // 数组下标从0开始,因此需要减1
}
2.2.3 顺序表的按值查找
按值查找操作通过给定的元素值来查找顺序表中该元素的位置。
cpp
// 在顺序表中查找值为e的元素,返回其位置(1 ≤ 位置 ≤ L->length),若不存在则返回0
int LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e) {
return i + 1; // 返回位置,数组下标转换为位置(从1开始)
}
}
return 0; // 未找到
}
2.2.4 顺序表插入元素
插入元素操作在顺序表的指定位置插入一个新的元素,并可能需要移动元素以保持顺序表的连续性。
cpp
// 在顺序表的第i个位置插入元素e(1 ≤ i ≤ L->length + 1)
int ListInsert(SqList *L, int i, ElemType e) {
if (i < 1 || i > L->length + 1 || L->length == MAX_SIZE) {
// 插入位置不合法或顺序表已满,可以进行错误处理,如返回错误码
return ERROR_CODE;
}
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1]; // 元素后移
}
L->data[i - 1] = e; // 插入新元素
L->length++; // 顺序表长度加1
return OK_CODE; // 插入成功
}
2.2.5 顺序表删除元素
删除元素操作从顺序表的指定位置删除一个元素,并可能需要移动元素以保持顺序表的连续性。
cpp
// 删除顺序表的第i个位置的元素(1 ≤ i ≤ L->length)
int ListDelete(SqList *L, int i, ElemType *e) {
if (i < 1 || i > L->length) {
// 删除位置不合法,可以进行错误处理,如返回错误码
return ERROR_CODE;
}
*e = L->data[i - 1]; // 保存被删除的元素
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j]; // 元素前移
}
L->length--; // 顺序表长度减1
return OK_CODE; // 删除成功
}
三、线性表的链式表示和实现
线性表的链式表示是一种常见的数据结构表示方式,它使用指针将一组数据元素按照其逻辑顺序连接起来。以下是对线性表链式表示的详细解释和实现:
3.1 链式表示的基本概念
- 结点:链式表示的基本单位是结点(Node),每个结点通常包含两部分信息:数据域(存放数据元素)和指针域(指向下一个结点)。
- 链表:n个结点链接成一个链表,即为线性表的链式存储结构。链表的起始结点称为头结点,尾结点的指针域为空(或指向一个特殊的结束标记)。
3.2 链表的类型
-
单向链表(Singly Linked List)
- 每个节点包含数据元素和一个指向下一个节点的引用(指针)。
- 只能从头节点开始顺序遍历到尾节点。
-
双向链表(Doubly Linked List)
- 每个节点包含数据元素、一个指向下一个节点的引用和一个指向前一个节点的引用。
- 可以在头尾两个方向上进行遍历。
-
带头节点的链表
- 在链表的首部增加一个额外的头节点,该头节点通常不存储实际数据,仅作为链表的起始点。
- 便于在链表的首部进行插入和删除操作,无需特殊处理头节点为空的情况。
-
不带头节点的链表
- 链表的首部即为第一个存储数据的节点。
- 在进行插入和删除操作时,需要特别注意头节点为空的情况。
-
循环链表(Circular Linked List)
- 链表的最后一个节点指向头节点,形成一个环状结构。
- 可以无限遍历链表,因为没有明确的终止点。
-
非循环链表
- 链表的最后一个节点指向空(NULL),表示链表的结束。
- 遍历链表时,当遇到指向空的指针时,表示已经到达链表的末尾。
-
静态链表
- 采用数组方式实现的链表结构,通过数组的下标来模拟链表的指针。
- 并非真正的链表,但在某些情况下可以替代链表进行使用。
-
跳表(Skip List)
- 一种随机化的数据结构,允许快速查询、插入、删除和更新操作。
- 通过多级索引来实现快速访问,类似于二分查找树,但实现更为简单。
3.3 单链表的实现
结点的定义
在C语言中,可以使用结构体(struct)来定义单链表的结点:
其中,ElemType
是数据元素的类型,可以是整型(int)、浮点型(float)等。SingleLinkNode
是结构体类型,SingleLinkList
是指向结构体类型的指针类型。
cpp
typedef struct SingleLinkNode {
ElemType data; // 存放数据
struct SingleLinkNode *next; // 存放指向下一个元素结点的指针
} SingleLinkNode, *SingleLinkList;
单链表的基本操作
(1)初始化
cpp
SingleLinkList_Init(SingleLinkList &SL) {
SL = new SingleLinkNode; // 生成新结点作为头结点,用头指针SL指向头结点
SL->next = NULL; // 头结点的指针域置空
}
(2)获取当前元素个数
cpp
int SingleLinkList_Length(SingleLinkList &SL) {
int count = 0; // 定义count计数
SingleLinkList p;
p = SL->next; // 定义p指向头结点后的第一个结点
while (p->next != NULL) { // 当指针的下一个地址不为空时,count+1
count++;
p = p->next; // p指向p的下一个地址
}
return count; // 返回count的数据
}
(3)插入数据元素
cpp
int SingleLinkList_Insert(SingleLinkList &SL, int i, ElemType e) {
SingleLinkNode *s, *p;
int j = 0;
p = SL;
while (p && j < i - 1) { // 寻找第i-1个结点
p = p->next;
++j;
}
if (!p || j > i - 1) { // i大于表长或者小于1
return 0;
}
s = new SingleLinkNode; // 生成新结点
s->data = e; // 将结点s的数据域置为e
s->next = p->next; // 新的结点的指针指向i结点
p->next = s; // 修改p的指针域指向新的结点
return 1;
}
(4)删除数据元素
cpp
int SingleLinkList_Delete(SingleLinkList &SL, int i) {
SingleLinkList p, q;
int j = 0;
p = SL;
while ((p->next) && (j < i - 1)) { // 查找第i-1个结点,p指向该结点
p = p->next;
++j;
}
if (!(p->next) || (j > i - 1)) { // 当i>n或i<1时,删除位置不合理
return 0;
}
q = p->next; // 临时保存被删结点的地址以备释放
p->next = q->next; // 改变删除结点前驱结点的指针域
delete q; // 释放删除结点的空间
return 1;
}
(5)取数据元素
cpp
ElemType SingleLinkGetElem(SingleLinkList &SL, int i, ElemType &e) {
SingleLinkNode *p;
int j;
p = SL->next; // 初始化,p指向首元结点
j = 1; // 计数器j初值赋为1
while (p && j < i) // 顺链域向后扫描,直到p为空或p指向第i个元素
{
p = p->next; // p指向下一个结点
++j; // 计数器j相应加1
}
if (!p || j > i) { // i值不合法(i > n 或 i <= 0)
return NULL; // 或其他表示错误的值/方式
}
e = p->data; // 取第i个结点的数据域
return e;
}
(6)销毁单链表
因为单链表的结点空间是程序动态申请的,而系统只负责自动回收程序中静态分配的内存空间,所以和顺序表相比,单链表需要增加一个销毁单链表的操作,用来在调用程序退出前释放动态申请的内存空间。
3.4 循环链表的定义与实现
循环链表是链式存储结构的一种特殊形式,其特点是表中最后一个节点的指针域指向头节点,从而使整个链表形成一个环状结构。这种结构使得链表中的元素可以无限循环地被访问,为某些特定场景下的操作提供了便利。
循环链表的实现
循环链表的实现通常涉及以下几个关键步骤和要点:
-
节点定义:
- 循环链表的节点通常包含数据域和指针域。数据域用于存储节点的数据,而指针域则用于指向下一个节点。
- 在C语言中,可以通过结构体(struct)来定义循环链表的节点。
-
链表初始化:
- 初始化循环链表时,需要创建一个头节点,并将头节点的指针域指向自己,表示一个空链表。
- 也可以根据需要初始化链表中的其他节点,并将它们链接起来形成环状结构。
-
插入操作:
- 在循环链表中插入节点时,需要找到插入位置的前一个节点,并修改其指针域以指向新节点。
- 新节点的指针域需要指向插入位置后的节点(在单循环链表中)或前一个节点(在双向循环链表中,如果考虑前驱指针的话)。
- 特别注意,当在链表头部插入新节点时,需要更新尾节点的指针域,使其指向新节点(如果链表之前为空,则尾节点也指向新节点)。
-
删除操作:
- 删除循环链表中的节点时,需要找到要删除的节点的前一个节点,并修改其指针域以跳过要删除的节点。
- 如果要删除的节点是头节点,则需要特别处理,更新尾节点的指针域和头节点的指针域。
- 释放要删除的节点的内存空间。
-
遍历操作:
- 遍历循环链表时,可以从头节点开始,依次访问每个节点,直到再次回到头节点。
- 由于循环链表是环状的,因此需要设置适当的停止条件来防止进入无限循环。常见的停止条件包括遍历指定次数、访问到某个特定节点或满足某个条件等。
-
其他操作:
- 根据需要,循环链表还可以实现其他操作,如查找节点、更新节点数据、计算链表长度等。
- 这些操作通常基于插入、删除和遍历等基本操作来实现。
示例代码(C语言)
以下是一个简单的单循环链表的示例代码,包括节点定义、链表初始化、插入、删除和遍历等操作:
cpp
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct Node {
int data; // 节点的数据
struct Node* next; // 指向下一个节点的指针
} Node;
// 初始化链表
Node* initList() {
Node* head = (Node*)malloc(sizeof(Node)); // 创建头节点
head->data = 0; // 头节点的数据设为0(或任意值,通常不存储实际数据)
head->next = head; // 头节点的指针指向自己,表示空链表
return head; // 返回头节点
}
// 插入节点(在尾部插入)
void insertAtTail(Node* head, int value) {
Node* newNode = (Node*)malloc(sizeof(Node)); // 创建新节点
newNode->data = value; // 新节点的数据设为要插入的值
Node* temp = head;
while (temp->next != head) { // 找到最后一个节点
temp = temp->next;
}
temp->next = newNode; // 最后一个节点的指针指向新节点
newNode->next = head; // 新节点的指针指向头节点,完成插入
}
// 删除节点(按值删除)
void deleteNode(Node* head, int value) {
Node* temp = head;
Node* prev = NULL;
while (temp->next != head) { // 遍历链表
if (temp->data == value) { // 找到要删除的节点
if (prev == NULL) { // 如果要删除的节点是头节点(虽然这里按值删除不会直接判断头节点,但为完整性考虑)
// 需要特别处理,更新尾节点的指针域和头节点的指针域(这里简化处理,只考虑非头节点情况)
// 实际上,在单循环链表中直接按值删除头节点是复杂的,因为需要知道尾节点
// 一种方法是先遍历找到尾节点,再处理头节点;另一种方法是维护一个额外的头节点指针或尾节点指针
// 这里为了简化,我们假设不会删除头节点(或将其视为特殊情况处理)
printf("Cannot delete head node directly in this simple implementation.\n");
return;
}
prev->next = temp->next; // 前一个节点的指针指向要删除节点的下一个节点
free(temp); // 释放要删除的节点的内存空间
return; // 删除成功,退出函数
}
prev = temp;
temp = temp->next;
}
printf("Node with value %d not found.\n", value); // 要删除的节点不存在
}
// 遍历链表
void traverseList(Node* head) {
Node* temp = head->next; // 从头节点的下一个节点开始遍历(跳过头节点,因为它不存储实际数据)
while (temp != head) { // 遍历直到再次回到头节点
printf("%d ", temp->data); // 输出节点的数据
temp = temp->next; // 移动到下一个节点
}
printf("\n"); // 输出换行符以区分不同的遍历结果
}
int main() {
Node* list = initList(); // 初始化链表
insertAtTail(list, 1); // 在尾部插入节点1
insertAtTail(list, 2); // 在尾部插入节点2
insertAtTail(list, 3); // 在尾部插入节点3
traverseList(list); // 遍历链表并输出节点的数据:1 2 3
deleteNode(list, 2); // 删除值为2的节点
traverseList(list); // 遍历链表并输出节点的数据:1 3
// 注意:这里没有处理头节点的删除情况,因为在这个简单实现中我们假设不会删除头节点
// 如果需要删除头节点,需要额外的逻辑来处理尾节点的指针域和更新头节点指针
return 0;
}
3.5 双链表的定义与实现
双链表(Doubly Linked List)是由一系列节点(Node)组成的链式数据结构。每个节点包含三个部分:数据域(Data)、前驱指针域(Prior Pointer)和后继指针域(Next Pointer)。
- 数据域:用于存储节点的数据。
- 前驱指针域:用于存储指向前一个节点的指针。
- 后继指针域:用于存储指向后一个节点的指针。
由于双链表节点具有前驱和后继两个指针,因此可以实现双向遍历,即可以从头节点遍历到尾节点,也可以从尾节点遍历到头节点。
双链表的实现
双链表的实现通常涉及以下几个基本操作:创建和初始化链表、插入节点、删除节点、查找节点和遍历链表。
- 创建和初始化链表
在创建双链表时,首先需要定义一个结构体来表示链表的节点。然后,通过动态内存分配创建一个头节点,并初始化其前驱和后继指针为NULL(或在带头双向循环链表中,将头节点的前驱和后继指针都指向头节点本身,形成循环)。
cpp
// 定义双链表节点结构体
struct DNode {
int data; // 数据域
struct DNode* prior; // 前驱指针域
struct DNode* next; // 后继指针域
};
// 创建双链表(带头节点)
struct DNode* createList() {
struct DNode* head = (struct DNode*)malloc(sizeof(struct DNode));
head->prior = head;
head->next = head;
return head;
}
- 插入节点
在双链表中插入节点时,需要指定插入位置,并修改插入位置前后节点的指针。插入操作可以分为头插、尾插和在指定位置插入。
- 头插:在头节点之后插入新节点,将新节点的后继指针指向头节点的后继节点,将头节点的后继节点的前驱指针指向新节点,然后将头节点的后继指针指向新节点,最后将新节点的前驱指针指向头节点。
- 尾插:在尾节点之前插入新节点,将新节点的前驱指针指向尾节点的前驱节点,将尾节点的前驱节点的后继指针指向新节点,然后将尾节点的前驱指针指向新节点,最后将新节点的后继指针指向尾节点。
- 在指定位置插入:先找到指定位置的前一个节点,然后修改前一个节点和新节点的指针,完成插入操作。
cpp
// 头插节点
void insertNodeAtHead(struct DNode* head, int data) {
struct DNode* newNode = (struct DNode*)malloc(sizeof(struct DNode));
newNode->data = data;
newNode->next = head->next;
newNode->prior = head;
head->next->prior = newNode;
head->next = newNode;
}
// 尾插节点(省略了部分代码,与头插类似,但操作相反)
// ...
// 在指定位置插入节点(省略了部分代码,需要找到指定位置的前一个节点)
// ...
- 删除节点
在双链表中删除节点时,需要找到要删除的节点,并修改其前后节点的指针。然后释放要删除节点的内存。
cpp
// 删除指定节点
void deleteNode(struct DNode* p) {
if (p == NULL || p->prior == NULL || p->next == NULL) return;
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
}
- 查找节点
在双链表中查找节点时,可以采用遍历的方法。从头节点开始,依次比较每个节点的数据域,直到找到目标节点或遍历完整个链表。
cpp
// 查找节点
struct DNode* findNode(struct DNode* head, int data) {
struct DNode* p = head->next;
while (p != head) {
if (p->data == data) return p;
p = p->next;
}
return NULL;
}
- 遍历链表
遍历双链表时,可以从头节点开始,依次访问每个节点,直到回到头节点(在循环链表中)或遍历完整个链表(在非循环链表中)。
cpp
// 遍历链表并打印节点数据
void traverseList(struct DNode* head) {
struct DNode* p = head->next;
while (p != head) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
四、顺序表和链表的比较
特性/数据结构 | 顺序表(Sequential List) | 链表(Linked List) |
---|---|---|
存储方式 | 使用连续的存储单元 | 使用不连续的存储单元,通过指针连接 |
访问速度 | 支持快速随机访问,时间复杂度为O(1) | 不支持快速随机访问,需要从头节点开始遍历,时间复杂度为O(n)(n为节点数) |
插入操作 | 在指定位置插入元素需要移动元素,平均时间复杂度为O(n) | 在指定位置插入元素只需修改指针,时间复杂度为O(1)(但需找到插入位置,若从头开始查找则平均时间复杂度为O(n))但无需移动数据 |
删除操作 | 删除指定位置的元素需要移动元素,平均时间复杂度为O(n) | 删除指定位置的元素只需修改指针,时间复杂度为O(1)(但需找到删除位置,若从头开始查找则平均时间复杂度为O(n))但无需移动数据 |
内存利用率 | 内存利用率高,因为存储单元是连续的 | 内存利用率相对较低,因为每个节点都需要额外的指针存储空间,且可能存在内存碎片 |
空间分配 | 需要预先分配固定大小的数组 | 动态分配内存,可以根据需要增加或减少节点 |
适用场景 | 适用于需要频繁访问元素的场景 | 适用于需要频繁插入和删除元素的场景 |
稳定性 | 插入和删除操作可能导致大量元素移动,稳定性较差 | 插入和删除操作只涉及指针修改,稳定性较好 |