数据结构------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为NULL
    Node* 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 10
    insertAtHead(&head, 10);
    insertAtHead(&head, 20);
    
    // 尾部插入操作演示:插入30 → 链表:20 10 30
    // 再次尾部插入40 → 链表:20 10 30 40
    insertAtTail(&head, 30);
    insertAtTail(&head, 40);
    
    // 在节点20(头节点的下一个节点head->next)之后插入50
    // 插入后链表:20 50 10 30 40
    insertAfterNode(head->next, 50);
    
    // 删除节点50(此时是head->next节点)
    // 删除后链表恢复:20 10 30 40
    Node* 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->3
    nodes[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),超过会导致栈溢出。

相关推荐
Tee xm3 分钟前
运维仙途 第2章 日志深渊识异常
linux·运维·服务器·日志
小彭努力中11 分钟前
13.THREE.HemisphereLight 全面详解(含 Vue Composition 示例)
开发语言·前端·javascript·vue.js·深度学习·数码相机·ecmascript
VinfolHu1 小时前
【JAVA】数据类型与变量:深入理解栈内存分配(4)
java·开发语言
三思而后行,慎承诺1 小时前
Kotlin和JavaScript的对比
开发语言·javascript·kotlin
嵌入式在学无敌大神2 小时前
Linux网络编程:TCP多进程/多线程并发服务器详解
linux·服务器·网络
一刀到底2112 小时前
从实列中学习linux shell5: 利用shell 脚本 检测硬盘空间容量,当使用量达到80%的时候 发送邮件
linux·运维·学习
言之。2 小时前
Go语言中的错误处理
开发语言·后端·golang
Ops菜鸟(Xu JieHao)3 小时前
Linux Nginx网站服务【完整版】
linux·运维·服务器·nginx·网站
不吃香菜?3 小时前
逻辑回归在信用卡欺诈检测中的实战应用
算法·机器学习·逻辑回归
Kay_Liang3 小时前
探究排序算法的奥秘(下):快速排序、归并排序、堆排序
java·数据结构·c++·python·算法·排序算法