数据结构--线性表

一、线性表的类型定义

  1. 数据元素类型

    线性表由一系列数据元素组成,这些数据元素可以是基本数据类型(如整型、浮点型、字符型等),也可以是复杂的数据类型(如结构体、类、指针等)。

  2. 存储结构

    线性表的存储结构可以是顺序存储或链式存储。

    • 顺序存储:使用连续的存储空间来存储线性表的元素,通常通过数组来实现。
    • 链式存储:使用非连续的存储空间来存储线性表的元素,每个元素(称为节点)包含数据域和指针域(或链接域),指针域用于指向下一个节点。
  3. 基本操作

    线性表的基本操作包括初始化、插入、删除、查找、遍历等。

    • 初始化:创建一个空的线性表。
    • 插入:在指定位置插入一个新元素。
    • 删除:删除指定位置的元素。
    • 查找:查找指定元素的位置或值。
    • 遍历:按顺序访问线性表中的每个元素。
  4. 类型定义方式

    线性表的类型定义可以使用多种方式,如使用结构体(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 特点

  1. 元素连续存储:顺序表的元素在内存中占据一块连续的存储空间,这使得通过下标访问元素变得非常高效,时间复杂度为O(1)。

  2. 随机访问:由于元素是连续存储的,因此可以像访问数组一样,通过下标直接访问顺序表中的任意元素。

  3. 存储密度高:顺序表不需要为元素之间的逻辑关系而增加额外的存储空间,因此存储密度较高。

  4. 插入和删除操作可能低效:在顺序表中插入或删除元素时,可能需要移动大量的元素,以保持元素的连续性。特别是在表的中间或靠近末尾的位置进行插入或删除操作时,效率较低。

2.1.2基本操作

  1. 初始化:为顺序表分配一块足够大的连续存储空间,并设置表的初始状态(如长度为0)。

  2. 插入:在顺序表的指定位置插入一个新的元素。如果插入位置合法且表未满,则将该位置及其后的元素依次后移一位,然后将新元素插入到指定位置。

  3. 删除:从顺序表中删除指定位置的元素。如果删除位置合法且表不为空,则将该位置后的元素依次前移一位,然后更新表的长度。

  4. 查找:根据给定的元素值,在顺序表中查找该元素的位置。如果找到,则返回其位置(下标);否则,返回一个特殊值(如-1)表示未找到。

  5. 更新:根据给定的位置和元素值,更新顺序表中指定位置的元素。

  6. 遍历:依次访问顺序表中的每个元素,并对它们进行某种处理(如打印、计算等)。

2.1.3 实现

顺序表通常使用数组来实现。在C语言中,可以使用静态数组或动态数组(如通过mallocrealloc函数分配的数组)来存储顺序表的元素。在C++中,可以使用std::vector等容器类来实现顺序表。

2.1.4 注意事项

  1. 预分配空间:在使用顺序表之前,通常需要预分配一块足够大的连续存储空间。如果实际存储的元素数量超过了预分配的空间大小,就会发生存储溢出错误。

  2. 动态扩展:为了处理存储溢出问题,可以使用动态扩展技术。当顺序表的空间不足时,可以重新分配一块更大的连续存储空间,并将原有的元素复制到新的存储空间中。然而,这种操作可能会导致性能下降和内存碎片问题。

  3. 边界检查:在进行插入、删除和查找等操作时,需要进行边界检查以确保操作的合法性。例如,插入位置应该在表的长度加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 链式表示的基本概念

  1. 结点:链式表示的基本单位是结点(Node),每个结点通常包含两部分信息:数据域(存放数据元素)和指针域(指向下一个结点)。
  2. 链表:n个结点链接成一个链表,即为线性表的链式存储结构。链表的起始结点称为头结点,尾结点的指针域为空(或指向一个特殊的结束标记)。

3.2 链表的类型

  1. 单向链表(Singly Linked List)

    • 每个节点包含数据元素和一个指向下一个节点的引用(指针)。
    • 只能从头节点开始顺序遍历到尾节点。
  2. 双向链表(Doubly Linked List)

    • 每个节点包含数据元素、一个指向下一个节点的引用和一个指向前一个节点的引用。
    • 可以在头尾两个方向上进行遍历。
  3. 带头节点的链表

    • 在链表的首部增加一个额外的头节点,该头节点通常不存储实际数据,仅作为链表的起始点。
    • 便于在链表的首部进行插入和删除操作,无需特殊处理头节点为空的情况。
  4. 不带头节点的链表

    • 链表的首部即为第一个存储数据的节点。
    • 在进行插入和删除操作时,需要特别注意头节点为空的情况。
  5. 循环链表(Circular Linked List)

    • 链表的最后一个节点指向头节点,形成一个环状结构。
    • 可以无限遍历链表,因为没有明确的终止点。
  6. 非循环链表

    • 链表的最后一个节点指向空(NULL),表示链表的结束。
    • 遍历链表时,当遇到指向空的指针时,表示已经到达链表的末尾。
  7. 静态链表

    • 采用数组方式实现的链表结构,通过数组的下标来模拟链表的指针。
    • 并非真正的链表,但在某些情况下可以替代链表进行使用。
  8. 跳表(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 循环链表的定义与实现

循环链表是链式存储结构的一种特殊形式,其特点是表中最后一个节点的指针域指向头节点,从而使整个链表形成一个环状结构。这种结构使得链表中的元素可以无限循环地被访问,为某些特定场景下的操作提供了便利。

循环链表的实现

循环链表的实现通常涉及以下几个关键步骤和要点:

  1. 节点定义

    • 循环链表的节点通常包含数据域和指针域。数据域用于存储节点的数据,而指针域则用于指向下一个节点。
    • 在C语言中,可以通过结构体(struct)来定义循环链表的节点。
  2. 链表初始化

    • 初始化循环链表时,需要创建一个头节点,并将头节点的指针域指向自己,表示一个空链表。
    • 也可以根据需要初始化链表中的其他节点,并将它们链接起来形成环状结构。
  3. 插入操作

    • 在循环链表中插入节点时,需要找到插入位置的前一个节点,并修改其指针域以指向新节点。
    • 新节点的指针域需要指向插入位置后的节点(在单循环链表中)或前一个节点(在双向循环链表中,如果考虑前驱指针的话)。
    • 特别注意,当在链表头部插入新节点时,需要更新尾节点的指针域,使其指向新节点(如果链表之前为空,则尾节点也指向新节点)。
  4. 删除操作

    • 删除循环链表中的节点时,需要找到要删除的节点的前一个节点,并修改其指针域以跳过要删除的节点。
    • 如果要删除的节点是头节点,则需要特别处理,更新尾节点的指针域和头节点的指针域。
    • 释放要删除的节点的内存空间。
  5. 遍历操作

    • 遍历循环链表时,可以从头节点开始,依次访问每个节点,直到再次回到头节点。
    • 由于循环链表是环状的,因此需要设置适当的停止条件来防止进入无限循环。常见的停止条件包括遍历指定次数、访问到某个特定节点或满足某个条件等。
  6. 其他操作

    • 根据需要,循环链表还可以实现其他操作,如查找节点、更新节点数据、计算链表长度等。
    • 这些操作通常基于插入、删除和遍历等基本操作来实现。

示例代码(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)。

  1. 数据域:用于存储节点的数据。
  2. 前驱指针域:用于存储指向前一个节点的指针。
  3. 后继指针域:用于存储指向后一个节点的指针。

由于双链表节点具有前驱和后继两个指针,因此可以实现双向遍历,即可以从头节点遍历到尾节点,也可以从尾节点遍历到头节点。

双链表的实现

双链表的实现通常涉及以下几个基本操作:创建和初始化链表、插入节点、删除节点、查找节点和遍历链表。

  • 创建和初始化链表

在创建双链表时,首先需要定义一个结构体来表示链表的节点。然后,通过动态内存分配创建一个头节点,并初始化其前驱和后继指针为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))但无需移动数据
内存利用率 内存利用率高,因为存储单元是连续的 内存利用率相对较低,因为每个节点都需要额外的指针存储空间,且可能存在内存碎片
空间分配 需要预先分配固定大小的数组 动态分配内存,可以根据需要增加或减少节点
适用场景 适用于需要频繁访问元素的场景 适用于需要频繁插入和删除元素的场景
稳定性 插入和删除操作可能导致大量元素移动,稳定性较差 插入和删除操作只涉及指针修改,稳定性较好
相关推荐
IronmanJay1 小时前
【LeetCode每日一题】——862.和至少为 K 的最短子数组
数据结构·算法·leetcode·前缀和·双端队列·1024程序员节·和至少为 k 的最短子数组
IT规划师4 小时前
数据结构 - 散列表,三探之代码实现
数据结构·散列表·哈希表
武子康5 小时前
大数据-187 Elasticsearch - ELK 家族 Logstash Filter 插件 使用详解
大数据·数据结构·elk·elasticsearch·搜索引擎·全文检索·1024程序员节
韭菜盖饭7 小时前
LeetCode每日一题3211---生成不含相邻零的二进制字符串
数据结构·算法·leetcode
czme7 小时前
线程和进程
linux·数据结构·计算机网络
J_admin9 小时前
数据结构 之 二叉树的遍历------先根遍历(五)
数据结构
黑不拉几的小白兔14 小时前
PTA L1系列题解(C语言)(L1_097 -- L1_104)
数据结构·算法·1024程序员节
秋说14 小时前
【数据结构 | PTA】懂蛇语
数据结构·c++
ChaoZiLL15 小时前
关于我的数据结构与算法——初阶第二篇(排序)
数据结构·算法
single59416 小时前
【综合算法学习】(第十篇)
java·数据结构·c++·vscode·学习·算法·leetcode