当前位置: 首页 > news >正文

数据结构------C语言经典题目(6)

1.数据结构都学了些什么?

1.基本数据类型

算数类型:

        char(字符)、int(整数)、float(单精度浮点数)、double(双精度浮点数)等。

枚举类型:

        enum,自定义一组命名的整形常量。例如颜色:enum Color {RED ,GREEN ,BLUE}:

2.构造数据类型

数组(Array):

        固定大小、连续存储的同类型元素集合;

        支持通过下标快速访问元素,但插入/删除操作效率低。

结构体(Struct):

        自定义的复合数据类型,可以包含不同类型的成员(如学生信息);

struct Student {char name[20];int age;float score;
};

联合体(Union):

        多个不同类型的变量共享同一段内存空间,同一时刻只能存储其中一个成员的值(节省内存)。

3.动态数据结构

通过指针动态分配内存,结构灵活。

链表:

        由节点组成,每个节点包含数据和指向下一个节点的指针。

        有单向链表、双向链表、循环链表等,插入/删除操作高效(无需移动大量元素),但访问效率低(需遍历)。

栈:

        遵循   后进先出  原则,可以通过数组或链表实现。

        常见操作:入栈、出栈、取栈顶元素。

队列:

        遵循   先进先出   原则,有入队、出队。

树:

        分层结构,由节点和边组成,根节点无父节点,子节点有唯一父节点。

        常见类型:二叉树(每个节点最多两个子节点)、二叉搜索树(左根右)、堆(用于优先队列)等。

图:    

        由顶点(节点)和边组成,用于表示复杂关系(社交网络、地图路径)。

        分为有向图、无向图、带权图。

4.数据结构的核心操作

查找:

        如顺序查找、二分查找(仅适用于有序数组)。

插入:

        在指定位置添加元素。

删除:

        移动指定元素。

排序:

        冒泡排序、快速排序、希尔排序。

遍历:

        按一定顺序访问元素。

5.指针与数据结构

        通过malloc()、calloc()等函数动态分配内存(如创建链表节点)。

        指针操作需注意内存泄漏(分配的内存未释放)和野指针(指向已释放的内存)问题。

6.应用场景

数组:

        适合需要快速随机访问、数据大小固定的场景(存储学生成绩)。

链表:

        适合频繁插入/删除、数据大小动态变化的场景。

栈:

        用于函数调用栈、表达式求值、括号匹配等。

队列:

        用于任务调度等。

树和图:

        用于文件系统目录结构等。

2.数据结构中的顺序表和链表有什么区别?

存储结构:

顺序表:

        1.存储方式:数据元素在内存中连续存储,逻辑上相邻的元素在物理地址上也相邻。

        2.实现方式:通常由数组实现,通过数组下标访问元素。

        3.内存分配:需要预先分配固定大小的内存空间,若数量动态变化会导致空间浪费或溢出。

链表:

        1.存储方式:数据元素分散存储,每个元素(节点)包含数据域和指针域,指针指向下一个节点的地址。

        2.实现方式:通过结构体和指针动态创建节点,节点之间通过指针链接。

        3.内存分配:按需动态分配内存,无需预先指定最大容量,比较灵活。

访问方式:

顺序表:

        随机访问:可以通过下标直接访问任意元素,时间复杂度为O(1)。

链表:        

        顺序访问:必须从表头开始逐个遍历节点,直到找到目标元素,时间复杂度为O(n)。

插入和删除操作:

顺序表:

        插入:在开头或中间任意位置插入元素,都需要移动后续所有元素。

        删除:删除中间或开头元素,需移动后续元素填补空缺。

        尾插尾删无需移动元素。

链表:

        插入/删除:只需修改指针指向,无需移动其他元素,但要找到插入位置的前驱节点。

        示例:在节点p后插入新节点:

        new_node  ->  next  =  p->next;

        p->next = new_node;

适用场景:

顺序表:       

        适合频繁随机访问的场景。

链表:

        适合频繁插入/删除的场景。

3.单向链表和双向链表有什么区别?

单向链表:

        每个节点包含一个数据域和一个指向下一个节点的指针(next)

Node1 → Node2 → Node3 → ... → NULL

        节点定义代码示例:

struct Node {int data;struct Node* next;
};

        遍历只能从头节点开始,沿next指针单向遍历(向后),无法反向访问前面的节点。

        插入节点时,只需修改当前节点的next 指针指向新节点,或新节点的next 指针指向后续节点。

        删除节点时,需找到要删除的节点的前驱节点,修改其next指针跳过要删除的节点。

        每个节点包含一个指针,内存占用相对较小。

双向链表:

        每个节点包含一个数据域、一个指向前一个节点的指针(prev)和一个指向下一个节点的指针(next)

NULL ← Node1 ↔ Node2 ↔ Node3 ← ... ← NULL

        节点定义代码示例:

struct Node {int data;struct Node* prev;struct Node* next;
};

        可以从头节点或尾节点开始,通过prev和next指针双向遍历。

        插入节点后,需同时修改新节点的prev(指向前驱节点)和next(指向后继节点)。

        删除节点时,可通过当前节点的prev指针直接找到前驱节点,通过next指针找到后继节点,修改两者的指针即可。

        每个节点包含两个指针(prev和next),内存占用比单向链表多约一倍。

4.什么是内存泄漏?如何排查和避免内存泄漏?

        内存泄漏是指程序动态分配的内存空间(如malloc、calloc、realloc等函数申请)在使用完毕后未被正确释放。即未调用free函数。导致这部分内存无法被系统重新分配利用的现象。

内存泄漏常见场景:

1.直接申请内存后未调用free释放

例如:

void function() 
{int *p = (int *)malloc(sizeof(int));// 使用 p// 未调用 free(p)
}  // p 离开作用域后,内存泄漏

2.重复释放或错误释放:

        释放已释放的指针(多次调用free(p)),导致程序崩崩溃。

        释放非动态分配的内存(局部变量的地址),导致段错误。

3.指针被覆盖:

        分配内存后,指针指向其他地址,导致原内存地址丢失,无法释放:

int *p = (int *)malloc(sizeof(int));
p = (int *)malloc(sizeof(int));  // 新分配覆盖了 p,原内存未释放
free(p);  // 仅释放了最后一次分配的内存

4.循环或条件分支中的分配未释放

        当循环或条件判断中分配内存,但某些分支未执行释放逻辑。

while (condition) 
{int *p = (int *)malloc(sizeof(int));if (some_case) {continue;  // 直接跳过释放}free(ptr);
}

如何排查:

1.手动排查:

检查所有malloc、calloc、realloc是否有对应的free,且释放次数正确。

        确保指针在释放后被置为NULL(避免野指针)。

2.使用内存检测工具

        通过memcheck根据动态检测内存泄漏,能精准定位未释放的内存及分配位置。

valgrind --leak-check=full ./your_program

3.调试器(GDB)

        在程序退出前检查堆内存状态,结合断点定位泄漏点。

如何避免:

        遵循:分配-使用-释放  的配对原则

        在释放内存后,立即将指针置为NULL,防止后续误操作(野指针)。

5.什么是内存碎片?如何避免内存碎片?

        内存碎片是指程序运行过程中,由于频繁地申请和释放内存,导致内存中出现大量不连续的小空闲块,这些空闲块虽然总容量足够,但无法满足较大内存块的分配请求,从而影响内存使用效率的现象。

分类:

1.内部碎片:

        当分配的内存块大于实际所需大小时,多余的空间未被使用,形成内部空闲空间。

2.外部碎片:

        多次分配和释放内存后,空闲内存被分割为不连续的小块,虽然总空闲内存足够,但无法找到单个足够大的连续块满足新的分配请求。

如何避免:

1.减少动态内存分配和释放的次数:

        尽量复用已分配的内存。

2.使用内存池:

        预先分配一大块内存,划分为多个固定大小的小块,通过池管理和回收,避免碎片化。

        分配/释放速度快(无需系统调用),碎片控制在池内。

// 简化的内存池结构
typedef struct {char* buffer;       // 池内存起始地址size_t block_size;  // 每个块大小size_t num_blocks;  // 块总数int* free_blocks;   // 记录可用块索引
} MemoryPool;

6.如何实现双向链表的插入?删除?

        示例代码如下:

// 定义双向链表节点结构
// 每个节点包含数据域(data)、前驱指针(prev)和后继指针(next)
typedef struct Node {int data;               // 存储节点数据struct Node* prev;      // 指向前驱节点的指针(NULL表示无前驱)struct Node* next;      // 指向后继节点的指针(NULL表示无后继)
} Node;// 创建新节点并初始化数据
// 参数:data - 节点存储的数据
// 返回:新创建的节点指针(内存分配失败时返回NULL)
Node* createNode(int data) 
{Node* newNode = (Node*)malloc(sizeof(Node));  // 分配节点内存if (newNode == NULL) {printf("内存分配失败!\n");exit(1);  // 终止程序防止空指针操作}newNode->data = data;        // 初始化数据域newNode->prev = NULL;        // 新节点初始无前驱newNode->next = NULL;        // 新节点初始无后继return newNode;              // 返回新节点指针
}// 在链表头部插入新节点
// 参数:head - 指向头节点指针的指针(用于修改头节点),data - 插入的数据
// 功能:将新节点插入到链表头部,更新头指针及前后指针关系
void insertAtHead(Node** head, int data) 
{Node* newNode = createNode(data);  // 创建新节点// 处理空链表情况:新节点成为唯一节点if (*head == NULL) {*head = newNode;  // 头指针指向新节点return;}// 非空链表处理:新节点成为新头节点newNode->next = *head;          // 新节点的后继指向原头节点(*head)->prev = newNode;        // 原头节点的前驱指向新节点*head = newNode;                // 更新头指针为新节点
}// 在链表尾部插入新节点
// 参数:head - 指向头节点指针的指针,data - 插入的数据
// 功能:遍历链表找到尾节点,将新节点连接到尾部
void insertAtTail(Node** head, int data) 
{Node* newNode = createNode(data);  // 创建新节点// 处理空链表情况:新节点成为唯一节点if (*head == NULL) {*head = newNode;  // 头指针指向新节点return;}// 查找尾节点:从头部开始遍历直到next为NULLNode* current = *head;while (current->next != NULL) {current = current->next;  // 移动到下一个节点}// 连接新节点到尾节点之后current->next = newNode;       // 尾节点的后继指向新节点newNode->prev = current;       // 新节点的前驱指向尾节点
}// 在指定节点之后插入新节点
// 参数:targetNode - 目标节点(新节点将插入到其之后),data - 插入的数据
// 功能:在目标节点之后插入新节点,处理前后节点的指针关系
void insertAfterNode(Node* targetNode, int data) 
{if (targetNode == NULL) {       // 检查目标节点是否存在printf("目标节点不存在!\n");return;}Node* newNode = createNode(data);  // 创建新节点// 新节点的后继指向目标节点的后继(可能为NULL)newNode->next = targetNode->next;// 新节点的前驱指向目标节点newNode->prev = targetNode;// 如果目标节点有后继节点,更新其后继的前驱指针if (targetNode->next != NULL) {targetNode->next->prev = newNode;}// 目标节点的后继指向新节点targetNode->next = newNode;
}// 删除指定节点
// 参数:head - 指向头节点指针的指针,targetNode - 待删除的目标节点
// 功能:从链表中移除目标节点,处理前后节点的指针连接并释放内存
void deleteNode(Node** head, Node* targetNode) 
{if (*head == NULL || targetNode == NULL) {  // 检查链表是否为空或节点是否存在printf("链表为空或目标节点不存在!\n");return;}// 处理删除头节点的情况:更新头指针if (*head == targetNode) {*head = targetNode->next;  // 头指针指向原头节点的后继}// 调整前驱节点的后继指针(如果存在前驱)if (targetNode->prev != NULL) {targetNode->prev->next = targetNode->next;  // 前驱的后继指向目标节点的后继}// 调整后继节点的前驱指针(如果存在后继)if (targetNode->next != NULL) {targetNode->next->prev = targetNode->prev;  // 后继的前驱指向目标节点的前驱}// 释放目标节点内存并置空指针(防止野指针)free(targetNode);targetNode = NULL;
}// 打印链表内容(从头部到尾部)
// 参数:head - 头节点指针
// 功能:遍历链表并输出每个节点的数据
void printList(Node* head) 
{Node* current = head;  // 当前节点从头部开始printf("双向链表内容: ");while (current != NULL) {  // 遍历直到NULL(链表末尾)printf("%d ", current->data);  // 输出当前节点数据current = current->next;       // 移动到下一个节点}printf("\n");
}// 主函数:演示双向链表操作
int main() 
{Node* head = NULL;  // 初始化空链表// 头部插入操作演示:插入10 → 链表:10// 再次头部插入20 → 链表:20 10insertAtHead(&head, 10);insertAtHead(&head, 20);// 尾部插入操作演示:插入30 → 链表:20 10 30// 再次尾部插入40 → 链表:20 10 30 40insertAtTail(&head, 30);insertAtTail(&head, 40);// 在节点20(头节点的下一个节点head->next)之后插入50// 插入后链表:20 50 10 30 40insertAfterNode(head->next, 50);// 删除节点50(此时是head->next节点)// 删除后链表恢复:20 10 30 40Node* nodeToDelete = head->next;  // 获取待删除节点(值为50的节点)deleteNode(&head, nodeToDelete);printList(head);  // 输出最终链表内容// 释放链表所有节点内存(防止内存泄漏)while (head != NULL) {Node* temp = head;       // 保存当前节点指针head = head->next;       // 头指针指向下一个节点free(temp);              // 释放当前节点内存}return 0;
}

7.怎么判断一个链表是否有环?

        可以使用快慢指针法来判断链表是否有环。

        快指针每次移动两步,慢指针每次移动一步。

        若链表存在环,快指针最终会追上慢指针。

        若快指针到达链表末尾(即指向NULL),则链表无环。

        示例代码如下:

// 定义链表节点结构
typedef struct ListNode {int val;                // 节点存储的整数值struct ListNode *next;  // 指向下一个节点的指针
} ListNode;// --------------------------
// 函数功能:判断链表是否存在环
// 输入参数:head - 链表头节点指针
// 输出参数:1 - 存在环;0 - 不存在环
// 算法:快慢指针法(弗洛伊德龟兔赛跑算法)
// 原理:快指针每次移动2步,慢指针每次移动1步。若存在环,快指针必然追上慢指针;若快指针到达链表末尾,则无环
// --------------------------
int hasCycle(ListNode *head) 
{// 处理特殊情况:空链表或单个节点(无后续节点,不可能形成环)if (head == NULL || head->next == NULL) {return 0;}// 初始化快慢指针:// 慢指针从头节点开始,每次移动1步// 快指针从头节点的下一个节点开始(领先1步),避免初始位置相同导致循环不执行(当链表有环时,至少需要2个节点才能形成环)ListNode *slow = head;ListNode *fast = head->next;// 循环条件:快慢指针未相遇(相遇则说明有环)while (fast != slow) {// 快指针到达链表末尾(无环):// 快指针每次移动2步,需检查当前节点和下一个节点是否为NULL,避免越界访问if (fast == NULL || fast->next == NULL) {return 0;  // 无环}// 慢指针移动1步slow = slow->next;// 快指针移动2步(先移动1步,再移动1步)fast = fast->next->next;}// 循环退出时,快慢指针相遇,说明存在环return 1;
}// --------------------------
// 函数功能:创建一个带环的链表(用于测试)
// 结构:1 -> 2 -> 3 -> 2(形成环,尾节点指向第二个节点)
// 返回值:链表头节点指针
// --------------------------
ListNode* createCycleList() 
{// 分配3个节点的内存空间ListNode *nodes = (ListNode*)malloc(3 * sizeof(ListNode));// 初始化节点值和连接关系nodes[0].val = 1;nodes[1].val = 2;nodes[2].val = 3;// 正常连接:1->2->3nodes[0].next = &nodes[1];nodes[1].next = &nodes[2];// 形成环:3->2(尾节点指向第二个节点,构成环)nodes[2].next = &nodes[1];return nodes;  // 返回头节点(第一个节点)
}// --------------------------
// 函数功能:创建一个无环的链表(用于测试)
// 结构:1 -> 2 -> 3 -> NULL(正常尾节点)
// 返回值:链表头节点指针
// --------------------------
ListNode* createAcyclicList() 
{// 分配3个节点的内存空间ListNode *nodes = (ListNode*)malloc(3 * sizeof(ListNode));// 初始化节点值和连接关系nodes[0].val = 1;nodes[1].val = 2;nodes[2].val = 3;// 正常连接:1->2->3->NULL(尾节点指向NULL,无环)nodes[0].next = &nodes[1];nodes[1].next = &nodes[2];nodes[2].next = NULL;return nodes;  // 返回头节点(第一个节点)
}int main() 
{// 测试带环链表ListNode *cycleHead = createCycleList();printf("带环链表检测结果:%s\n", hasCycle(cycleHead) ? "有环" : "无环");  // 预期输出"有环"// 测试无环链表ListNode *acyclicHead = createAcyclicList();printf("无环链表检测结果:%s\n", hasCycle(acyclicHead) ? "有环" : "无环");  // 预期输出"无环"// 释放内存(避免内存泄漏)// 注意:实际使用中需确保所有动态分配的内存都被正确释放free(cycleHead);free(acyclicHead);return 0;
}

主函数的打印逻辑也可以这么写:

int result = hasCycle(cycleHead); // 获取返回值(1或0)
if (result)         // 等价于 if (result != 0)
{                    printf("带环链表检测结果:有环\n");
} 
else 
{printf("带环链表检测结果:无环\n");
}

8.队列和栈有什么区别?在什么场景下使用?

队列:

        1.先进先出

        2.只能在队尾插入,队头删除

        3.入队、出队、查看队头

        4.适合“顺序处理”数据,如任务调度、缓冲区管理

        5.常用链表(避免数组的固定大小限制)或循环数组

        队列就像排队买票,先到的人先处理。数据只能从队尾加入,从队头移除。

        队列需要手动实现,一般用链表(动态分配内存,避免固定大小限制)或循环数组。

栈:

        1.后进先出

        2.只能在栈顶进行插入和删除

        3.压栈、弹栈、查看栈顶

        4.适合“回溯”的场景,如函数调用、表达式求值

        5.可以用数组或链表实现,数组实现需注意栈溢出

        栈就像一叠盘子,最后放上去的盘子最先被拿走,插入和删除只能在栈顶进行,例如函数调用时的参数传递和局部变量存储。

        栈的内存管理是由编译器自动管理的,用于存储局部变量、函数参数等,内存分配和释放效率高,但大小固定(通常只有几MB),超过会导致栈溢出。

相关文章:

  • protothread协程库实现非阻塞延时(无操作系统)
  • LangChain 中主流的 RAG 实现方式
  • 第5.5章:ModelScope-Agent:支持多种API无缝集成的开源框架
  • Golang | 自行实现并发安全的Map
  • 运维打铁:Mysql 分区监控以及管理
  • 分享Matlab成功安装Support Package硬件支持包的方法
  • 通过音频的pcm数据格式利用canvas绘制音频波形图
  • GStreamer 简明教程(十一):插件开发,以一个音频生成(Audio Source)插件为例
  • Centos7.2安装Xmap
  • 司法考试模拟考试系统
  • LLM Graph Rag(基于LLM的事件图谱RAG)
  • 红黑树——如何靠控制色彩实现平衡的?
  • 记录搭建自己应用中心
  • OpenAI 推出「轻量级」Deep Research,免费用户同享
  • CSS 入门全解析
  • 0. Selenium工具的安装
  • deep鼠标跟随插件
  • 跟着尚硅谷学vue-day5
  • NVIDIA 高级辅助驾驶汽车安全系统 系列读后感(1)
  • 数据湖DataLake和传统数据仓库Datawarehouse的主要区别是什么?优缺点是什么?
  • “五一”假期倒计时,节前错峰出游机票降价四成
  • 观察|英国航母再次部署印太,“高桅行动”也是“高危行动”
  • 六部门:进一步优化离境退税政策扩大入境消费
  • 破解160年基因谜题,我国科学家补上豌豆遗传研究最后拼图
  • 马上评丨马拉松“方便门”被处罚,是一针清醒剂
  • 商务部:美方应彻底取消所有对华单边关税措施