数据结构---链表

news/2024/9/18 9:50:34/ 标签: 数据结构, 链表, linux, c语言

指针和数组 

  • 数组的用途:

    • 固定大小的存储: 数组用于存储固定大小的一组相同类型的元素。数组的大小在声明时必须指定,并且在程序运行期间不能改变。
    • 访问效率高: 数组允许通过下标进行快速访问,时间复杂度为 O(1)。
    • 内存连续性: 数组的元素在内存中是连续存储的,这对于某些算法或硬件操作非常有利。
  • 指针的用途:

    • 动态内存分配: 指针可以用于动态分配内存(如通过 malloccalloc 在堆上分配内存)。这样可以在运行时灵活地管理内存,而不需要预先确定数据的大小。
    • 灵活的数据结构: 指针是构建动态数据结构(如链表、树、图等)的基础。通过指针可以轻松实现元素间的动态链接和调整。
    • 传递大对象: 使用指针可以避免在函数间传递大对象时的复制开销。通过传递指针,只传递地址,节省了内存和处理时间。
    • 指针运算: 指针支持多种运算,如加减、比较等,可以灵活地操作内存。

指针使用过程中可能遇到的问题

1.内存管理复杂

  • 动态内存分配与释放: 使用指针动态分配内存时(如通过 malloccalloc),需要手动释放内存(使用 free)。忘记释放内存会导致内存泄漏,频繁分配和释放则可能导致内存碎片化。

  • 悬空指针 (Dangling Pointer): 当指针指向的内存已经被释放,但指针仍然在使用时,就会出现悬空指针。这可能导致程序崩溃或出现未定义行为。

2. 指针运算的复杂性

  • 指针算术: 对指针进行加减运算时,需要非常谨慎。错误的指针算术操作可能导致访问无效或错误的内存地址,导致程序崩溃或数据损坏。

  • 多级指针: 使用指向指针的指针(如 int** p)会使代码复杂化,容易引入错误。多级指针的解引用和操作需要特别小心。

3. 指针类型转换

  • 类型不匹配: 不同类型指针之间的转换需要小心处理,尤其是将 void* 转换为具体类型的指针时,必须确保转换后的指针能够正确访问内存,否则会导致未定义行为。

  • 指针对齐: 某些平台上,指针指向的地址可能要求对齐。如果不遵守对齐要求,可能会导致性能下降甚至崩溃。

4. 指针和数组的混淆

  • 数组名与指针的混淆: 虽然数组名可以被视为指针,但它们并不完全相同。数组名是一个常量指针,不能被修改;而指针可以指向任何内存位置。理解这两者的区别对于正确使用非常重要。

  • 多维数组与指针: 处理多维数组时,理解其在内存中的布局和如何通过指针访问各元素可能比较复杂,尤其是在传递给函数时。

5. 指针的初始化问题

  • 未初始化指针: 如果指针在使用前未被初始化,它将指向一个未知的内存地址。使用这样的指针进行操作可能会导致不可预测的结果或程序崩溃。

  • NULL指针: 使用指针前,通常需要检查其是否为 NULL,以防止对空指针进行解引用操作,这会导致程序崩溃。

6. 并发和多线程环境中的指针

  • 数据竞争 (Data Race): 在多线程环境中,如果多个线程同时访问或修改同一个指针指向的内存,而没有适当的同步措施,可能会导致数据竞争,进而导致不可预测的行为。

  • 共享指针的管理: 在并发编程中,共享指针的管理是一个挑战,特别是在需要安全地共享和释放资源时。

7. 指针错误调试困难

  • 调试复杂: 指针错误(如悬空指针、野指针)往往很难调试和定位,因为错误通常不会立即显现,而是可能在其他地方引发问题。

  • 未定义行为: 由于指针错误导致的未定义行为,使得问题具有随机性和不可重复性,进一步增加了调试的难度。

8. 指针的安全性问题

  • 缓冲区溢出 (Buffer Overflow): 由于指针可以直接操作内存,如果对内存访问不加限制,容易造成缓冲区溢出,导致内存损坏,甚至引发安全漏洞。

  • 指针攻击: 恶意代码可能通过操纵指针导致程序执行任意代码或进行非法访问。因此,指针的使用安全性需要特别注意。

9. 指针与函数指针的复杂性

  • 函数指针: 使用函数指针可以实现回调函数和动态函数调用,但其语法复杂且容易出错。函数指针的声明、初始化和调用需要非常谨慎。

10. 跨平台指针差异

  • 不同平台的指针大小: 指针的大小可能随平台变化(如32位系统中为4字节,64位系统中为8字节),这会影响到内存操作和数据结构的设计。

  • 指针的字节序: 不同平台可能使用不同的字节序(大端或小端),这在指针操作中可能带来问题,尤其是在网络编程或文件读写中。

11. 指针的深层拷贝与浅层拷贝

  • 深拷贝 vs 浅拷贝: 使用指针时,特别是在拷贝复杂数据结构时,需要理解深拷贝和浅拷贝的区别。浅拷贝只复制指针本身,而深拷贝则会复制指针指向的数据。错误的拷贝方式可能导致数据共享和意外修

 

部分存储结构

1. 顺序存储结构

  • 定义: 数据元素按顺序存储在内存的连续地址空间中。
  • 特点:
    • 每个元素的地址是固定的,并且可以通过数组下标直接访问。
    • 适合实现随机访问操作,时间复杂度为O(1)。
    • 插入和删除操作通常需要移动大量元素,效率较低。
  • 应用: 数组、顺序表(如 C 语言中的 int arr[])。
  • 示例: 在数组 int arr[5] = {1, 2, 3, 4, 5}; 中,arr[2] 直接访问第三个元素 3

2. 链式存储结构

  • 定义: 数据元素存储在内存的任意位置,元素之间通过指针相连形成一个链表结构。
  • 特点:
    • 不要求数据元素在内存中是连续的,内存利用率高。
    • 插入和删除操作高效,因为只需修改指针即可。
    • 访问操作需要从头遍历,时间复杂度为O(n)。
  • 应用: 单链表、双向链表、循环链表等。
  • 示例: 在单链表中,每个节点包含数据和指向下一个节点的指针,如 struct Node { int data; Node* next; }

3. 索引存储结构

  • 定义: 在存储数据的同时,建立一个额外的索引表来加速数据的查找。
  • 特点:
    • 通过索引可以快速定位数据位置,查找效率高。
    • 适合大规模数据的查找和排序操作。
    • 维护索引需要额外的空间和时间开销。
  • 应用: 数据库索引、跳表(Skip List)、B树、B+树等。
  • 示例: 在数据库中,给一个表建立索引后,可以通过索引快速查找到所需的记录。

4. 散列存储结构

  • 定义: 通过散列函数将数据映射到存储空间的特定位置(散列表),并以这种方式进行存储和查找。
  • 特点:
    • 查找、插入和删除操作的平均时间复杂度为O(1)。
    • 可能出现哈希冲突,需要采用解决方案如链地址法或开放地址法。
  • 应用: 哈希表(如 C++ 中的 std::unordered_map)、哈希集合。
  • 示例: 使用哈希函数 h(key) = key % table_size 将键值 key 映射到哈希表的位置。

1. 有头链表(Headed Linked List)

定义

有头链表是指在链表的最前面有一个特殊的节点,即头节点(head node)。头节点不存储实际的数据,它的作用是指向链表的第一个数据节点,或者作为链表的起点。

结构特点
  • 头节点: 有头链表有一个头节点,它始终存在,即使链表为空。头节点的 next 指针指向链表的第一个数据节点。
  • 统一的操作: 由于头节点的存在,链表的插入、删除、查找等操作可以统一处理,即不需要特殊处理链表的第一个节点。
  • 易于管理: 头节点作为链表的入口,使得链表管理更加简洁,无需担心链表为空时的特殊情况。
优点
  • 简化操作: 插入、删除操作在链表的头部位置时,逻辑更简单,因为头节点总是存在,不需要额外判断链表是否为空。
  • 更好的一致性: 头节点提供了链表操作的一致性,无论链表是否为空,操作的逻辑都可以保持一致。
缺点
  • 额外的空间开销: 头节点虽然不存储数据,但依然占用一定的内存空间。

2. 无头链表(Headless Linked List)

定义

无头链表是指链表没有头节点,第一个节点即是链表的第一个数据节点。链表的起点直接指向第一个数据节点,如果链表为空,则该指针为 NULL

结构特点
  • 没有头节点: 链表的首节点即为第一个数据节点,链表开始的指针直接指向这个节点。
  • 首节点的特殊处理: 由于没有头节点,在执行插入、删除等操作时,如果操作的是第一个节点,通常需要特殊处理。
优点
  • 节省内存: 无头链表没有头节点,因此节省了一个节点的内存开销。
  • 简单结构: 链表结构简单,直接从第一个数据节点开始,不需要头节点作为辅助。
缺点
  • 操作复杂性增加: 在进行插入、删除操作时,如果涉及到第一个节点,需要特殊判断和处理,代码可能变得更复杂。
  • 易出错: 由于没有头节点保护,在处理空链表或首节点操作时,容易出现错误,如空指针引用等。

 无头链表中常见的操作

 

1. 初始化链表

无头链表的初始化通常是将头指针设置为 NULL,表示链表为空。

struct Node {int data;struct Node* next;
};struct Node* head = NULL;  // 初始化为空链表

 

2. 插入节点

a. 在链表头部插入节点

  • 如果要在链表的头部插入节点,需要创建一个新节点,并将其 next 指针指向当前的 head,然后更新 head 指向新节点。
    void insertAtHead(struct Node** head, int newData) {struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = newData;newNode->next = *head;*head = newNode;
    }
    

    b. 在链表中间或尾部插入节点

  • 对于非头部的插入,需要遍历链表找到合适的位置,然后插入节点。
void insertAfter(struct Node* prevNode, int newData) {if (prevNode == NULL) {printf("The given previous node cannot be NULL.\n");return;}struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = newData;newNode->next = prevNode->next;prevNode->next = newNode;
}

 

3. 删除节点

a. 删除头节点

  • 如果要删除链表的头节点,需要将 head 指针指向第二个节点,并释放原头节点的内存。
    void deleteHead(struct Node** head) {if (*head == NULL) return;  // 空链表,直接返回struct Node* temp = *head;  // 暂存当前头节点*head = (*head)->next;      // 将头指针移向下一个节点free(temp);                 // 释放原头节点的内存
    }
    

    b. 删除指定节点

  • 对于删除非头节点的情况,需要遍历链表找到要删除的节点的前一个节点,然后调整指针跳过要删除的节点。
    void deleteNode(struct Node** head, int key) {struct Node* temp = *head, *prev = NULL;// 如果要删除的是头节点if (temp != NULL && temp->data == key) {*head = temp->next;  // 将头指针移向下一个节点free(temp);          // 释放原头节点return;}// 搜索要删除的节点,记录其前一个节点while (temp != NULL && temp->data != key) {prev = temp;temp = temp->next;}// 如果没有找到要删除的节点if (temp == NULL) return;// 跳过要删除的节点prev->next = temp->next;free(temp);  // 释放要删除的节点
    }
    

    4. 查找节点

  • 在无头链表中查找节点需要从头开始遍历,直到找到目标数据或链表结束。
struct Node* search(struct Node* head, int key) {struct Node* current = head;  // 从头节点开始while (current != NULL) {if (current->data == key) {return current;  // 找到节点}current = current->next;}return NULL;  // 如果没有找到
}

 

5. 遍历链表

  • 遍历链表即依次访问链表中的每个节点,通常用于打印链表元素或执行某种操作。

 

void printList(struct Node* head) {struct Node* current = head;  // 从头节点开始while (current != NULL) {printf("%d -> ", current->data);current = current->next;}printf("NULL\n");  // 表示链表结束
}

6. 链表长度

  • 通过遍历整个链表,可以计算出链表的长度。
    int getLength(struct Node* head) {int length = 0;struct Node* current = head;while (current != NULL) {length++;current = current->next;}return length;
    }
    

    在无头链表中使用结构体记录链表长度

1. 定义链表结构体

首先,定义一个链表结构体 LinkedList,其中包括链表的头指针和一个长度字段 length,用于记录链表中节点的数量。

#include <stdio.h>
#include <stdlib.h>// 定义链表节点结构体
struct Node {int data;struct Node* next;
};// 定义链表结构体,包含头指针和长度
struct LinkedList {struct Node* head;int length;
};// 初始化链表
void initList(struct LinkedList* list) {list->head = NULL;list->length = 0;
}

 

2. 插入节点时更新长度

在插入节点时,无论是在链表头部还是中间或尾部,都需要更新 length 字段

// 在链表头部插入节点
void insertAtHead(struct LinkedList* list, int newData) {struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = newData;newNode->next = list->head;list->head = newNode;list->length++;  // 更新链表长度
}// 在链表中间或尾部插入节点
void insertAfter(struct Node* prevNode, int newData, struct LinkedList* list) {if (prevNode == NULL) {printf("The given previous node cannot be NULL.\n");return;}struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = newData;newNode->next = prevNode->next;prevNode->next = newNode;list->length++;  // 更新链表长度
}

 

3. 删除节点时更新长度

类似地,在删除节点时也需要更新 length 字段。

// 删除头节点
void deleteHead(struct LinkedList* list) {if (list->head == NULL) return;  // 空链表,直接返回struct Node* temp = list->head;  // 暂存当前头节点list->head = list->head->next;   // 将头指针移向下一个节点free(temp);                      // 释放原头节点list->length--;  // 更新链表长度
}// 删除指定节点
void deleteNode(struct LinkedList* list, int key) {struct Node* temp = list->head;struct Node* prev = NULL;// 如果要删除的是头节点if (temp != NULL && temp->data == key) {list->head = temp->next;  // 将头指针移向下一个节点free(temp);               // 释放原头节点list->length--;           // 更新链表长度return;}// 搜索要删除的节点,记录其前一个节点while (temp != NULL && temp->data != key) {prev = temp;temp = temp->next;}// 如果没有找到要删除的节点if (temp == NULL) return;// 跳过要删除的节点prev->next = temp->next;free(temp);  // 释放要删除的节点list->length--;  // 更新链表长度
}

4. 获取链表长度

由于长度已经在插入和删除操作时自动更新,获取链表长度只需要读取 length 字段即可。

int getLength(struct LinkedList* list) {return list->length;
}

5. 遍历链表

遍历链表的操作和之前的链表操作基本相同,只不过这次是在 LinkedList 结构体中访问 head 指针。

void printList(struct LinkedList* list) {struct Node* current = list->head;while (current != NULL) {printf("%d -> ", current->data);current = current->next;}printf("NULL\n");  // 表示链表结束
}


http://www.ppmy.cn/news/1520512.html

相关文章

网络安全面试经验分享:蘑菇街/网络安全

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 蘑菇街 介绍…

蓝牙协议栈API分析

蓝牙协议栈API分析是一个复杂但重要的任务&#xff0c;它涉及到蓝牙通信的各个方面&#xff0c;包括设备发现、连接建立、数据传输以及安全管理等。以下是对蓝牙协议栈API的详细分析&#xff0c;旨在提供一个全面的视角。 一、蓝牙协议栈概述 蓝牙协议栈是蓝牙技术实现的基础…

解决reCaptcha v2 Invisible:识别和参数

概述 reCaptcha v2 Invisible是一种旨在提供安全性而不打扰用户体验的验证码类型。与传统的验证码不同&#xff0c;reCaptcha v2 Invisible在检测到可疑活动时才会要求用户进行互动。本文将引导您如何使用CapSolver API识别并解决reCaptcha v2 Invisible挑战。 什么是reCaptc…

ChatGPT与R语言融合技术在生态环境数据统计分析、绘图、模型中的实践与进阶应用

自2022年GPT&#xff08;Generative Pre-trained Transformer&#xff09;大语言模型的发布以来&#xff0c;它以其卓越的自然语言处理能力和广泛的应用潜力&#xff0c;在学术界和工业界掀起了一场革命。在短短一年多的时间里&#xff0c;GPT已经在多个领域展现出其独特的价值…

计算机网络-VRRP切换与回切过程

前面我们学习了VRRP选举机制&#xff0c;根据VRRP优先级与IP地址确定主设备与备份设备&#xff0c;这里继续进行主备切换与主备回切以及VRRP抢占模式的学习。 一、VRRP主备切换 主备选举时根据优先级选择主设备&#xff0c;状态切换为Master状态&#xff0c;那当什么时候会切换…

科研学习|论文解读——OceanGPT:用于海洋科学任务的大型语言模型

摘要 海洋覆盖我们星球表面70%以上&#xff0c;对于理解生命的丰富储备和生物多样性至关重要。鉴于海洋在调节全球气候和支持经济中的关键作用&#xff0c;海洋科学研究具有重大意义。最近&#xff0c;大型语言模型&#xff08;LLMs&#xff09;的进步改变了科学的范式。尽管在…

Linux下递归设置目标目录及其子目录和文件的权限

〇、背景 本文旨在简单介绍一个在Linux环境下批量修改目录及其子目录和文件的权限的方法。 一、实现 首先新建一个shell脚本文件&#xff0c;使用指令$ vi chmod.sh&#xff0c;然后在文件中输入下述代码。 #!/bin/bashOFFSET_INDEX" " DIR_MODE755 FILE_MODE664…

Oracle---PAG程序全局区的组成:堆栈区、会话区、游标区、排序区

文章目录 PGA程序全局区PGA主要内容1、排序区&#xff08;SORT AREA&#xff09;**为什么给排序设置合理的排序区大小** 2、会话区&#xff08;USER SESSON DATA&#xff09;3、堆栈区保存变量信息(STACK SPACE)4、游标区 (CURSOR STATE) PGA程序全局区 程序全局区或进程全局区…

AN7536PT时钟电路

目录 1 时钟电路概述2 时钟晶振电路2.1 需求分析2.2 晶振选型&#xff08;Datasheet表5-7解读&#xff09;2.3 设计晶振电路&#xff08;表4-1、图5-4&#xff09; 1 时钟电路概述 时钟电路是一种用于产生稳定、周期性脉冲信号的电子电路。它通常由晶体振荡器和相关逻辑电路组…

Luminar Neo for Mac智能图像处理软件【操作简单,轻松上手】

Mac分享吧 文章目录 效果一、下载软件二、开始安装1、双击运行软件&#xff0c;将其从左侧拖入右侧文件夹中&#xff0c;等待安装完毕2、应用程序显示软件图标&#xff0c;表示安装成功 三、运行测试安装完成&#xff01;&#xff01;&#xff01; 效果 一、下载软件 下载软件…

Hackme靶场渗透攻略

步骤一&#xff0c;注册登录进去 步骤二&#xff0c;点击search 我们发现有很多书 步骤三&#xff0c;搜索一本书抓包发放到重放器 步骤四&#xff0c;数据改为1*&#xff0c;复制数据包到1.txt&#xff0c;然后打开sqlmap 步骤五&#xff0c;sqlmap查看当前数据库 python s…

阿尔茨海默病症识别+图像识别Python+人工智能+深度学习+TensorFlow+机器学习+卷积神经网络算法

一、介绍 阿尔茨海默病症识别。使用Python作为主要编程语言进行开发&#xff0c;基于深度学习等技术使用TensorFlow搭建ResNet50卷积神经网络算法&#xff0c;通过对病症图片4种数据集进行训练[‘轻度痴呆’, ‘中度痴呆’, ‘非痴呆’, ‘非常轻微的痴呆’]&#xff0c;最终得…

TeeChart助力科研软件:高效实现数据可视化

在当今的科学研究中&#xff0c;数据可视化已经成为理解和传播复杂信息的关键工具。尤其是在物理研究领域&#xff0c;科学家们经常需要处理大量的数据&#xff0c;并通过可视化将这些数据转化为更易理解的形式。TeeChart作为一个强大且灵活的图形展示工具&#xff0c;能够帮助…

前端按钮通过浏览器下载附件

html <a click"downloadAttach(record.memoryAddress)">下载附件</a> js downloadAttach(url){var fileUrl window._CONFIG[staticDomainURL] url;window.open(fileUrl); } 配置文件 window._CONFIG[staticDomainURL] http://127.0.0.1:3000/xxx…

Spring Cloud Gateway的使用

Spring Cloud Gateway的使用 1. Spring Cloud Gateway原理2. Spring Boot项目中集成Spring Cloud Gateway2.1 创建项目与添加依赖2.2 配置网关 3. 高级功能与实践**3.1 配置过滤器****3.2 分组路由** 4. 监控与故障处理5. 部署与持续集成 在微服务架构中&#xff0c;服务发现、…

计算机网络(一) —— 网络基础入门

目录 一&#xff0c;关于网络 二&#xff0c;协议 2.1 协议是什么&#xff0c;有什么用&#xff1f; 2.2 协议标准谁定的&#xff1f; 2.3 协议分层 2.4 OSI 七层模型 2.5 TCP/IP 四层模型 三&#xff0c;网络传输基本流程 3.1 局域网中两台主机通信* 3.2 报文的封装与…

uniapp网站和微信小程序 添加 百度统计

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、首先&#xff0c;需要在百度统计平台注册一个账户或登录现有的账户二、新建站点(应用)、添加代码三、代码获取与安装1.在官方网站 新增应用&#xff0c;根据官方…

kernel底层的蓝牙开发流程

kernel底层的蓝牙开发流程是一个复杂且细致的过程&#xff0c;它涉及到从内核配置、驱动编写、工具编译到最终的设备调试等多个环节。以下是一个详细的蓝牙开发流程&#xff0c;旨在为读者提供一个全面的视角。 一、前期准备 1. 确定开发环境 首先&#xff0c;需要确定开发所…

复数遍历4联通区域

怎么理解虚数和复数&#xff1f; - 知乎

python-禁止抽烟

题目描述 小理的朋友有 n 根烟&#xff0c;他每吸完一根烟就把烟蒂保存起来&#xff0c;k&#xff08; k>1&#xff09;个烟蒂可以换一个新的烟&#xff0c;那么小理的朋友最终能吸到多少根烟呢&#xff1f; 与某些脑筋急转弯不同的是&#xff0c;小理的朋友并不能从异次元借…