19、数据结构与动态内存管理:单链表与文件I/O

数据结构与动态内存管理:单链表与文件I/O

单链表基础

单链表是一种简单的数据结构,用于在堆中存储可变数量的项。链表的最后一个节点的指针为 NULL ,表示链表结束。其节点结构定义如下:

#define NAME_SIZE 20    // Max number of characters in a name 
/** 
 * A node in the linked list 
 */ 
struct linkedList { 
    struct linkedList* next;    // Next node 
    char name[NAME_SIZE];       // Name of the node 
};

next 指针指向下一个节点(或为 NULL ), name 数组最多存储 20 个字符。

单链表操作
  1. 添加节点
    • 步骤:
      1. 创建新节点。
      2. 使新节点的 next 指针指向当前链表的头节点。
      3. 更新链表头指针指向新节点。
    • 代码示例:
static void addName(void) 
{ 
    printf("Enter word to add: "); 
    char line[NAME_SIZE];       // Input line 
    if (fgets(line, sizeof(line), stdin) == NULL) 
        return; 
    if (line[strlen(line)-1] == '\n') 
        line[strlen(line)-1] = '\0'; 
    // Get a new node. 
    struct linkedList* newNode = malloc(sizeof(*newNode)); 
    strncpy(newNode->name, line, sizeof(newNode->name)-1); 
    newNode->name[sizeof(newNode->name)-1] = '\0'; 
    newNode->next = theList; 
    theList = newNode; 
}
  1. 打印链表
    • 规则:从第一个节点开始,依次打印节点的 name ,直到链表结束。
    • 代码示例:
for (const struct linkedList* curNode = theList; 
     curNode != NULL; 
     curNode = curNode->next){ 
    printf("%s, ", curNode->name); 
}
  1. 删除节点
    • 步骤:
      1. 遍历链表,找到要删除的节点。
      2. 若要删除的是头节点,更新链表头指针。
      3. 若不是头节点,将前一个节点的 next 指针指向要删除节点的下一个节点。
      4. 释放要删除节点的内存。
    • 代码示例:
static void deleteWord(void) 
{ 
    printf("Enter word to delete: "); 
    char line[NAME_SIZE];       // Input line 
    if (fgets(line, sizeof(line), stdin) == NULL) 
        return; 
    if (line[strlen(line)-1] == '\n') 
        line[strlen(line)-1] = '\0'; 
    struct linkedList* prevNode = NULL; // Pointer to previous node 
    for (struct linkedList* curNode = theList; 
         curNode != NULL; 
         curNode = curNode->next) { 
        if (strcmp(curNode->name, line) == 0) { 
            if (prevNode == NULL) { 
                theList = curNode->next; 
            } else { 
                prevNode->next = curNode->next; 
            } 
            free(curNode); 
            curNode = NULL; 
            return; 
        } 
        prevNode = curNode; 
    } 
    printf("WARNING: Node not found %s\n", line); 
}
完整程序示例
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <stdbool.h> 

#define NAME_SIZE 20    // Max number of characters in a name 
/** 
 * A node in the linked list 
 */ 
struct linkedList { 
    struct linkedList* next;    // Next node 
    char name[NAME_SIZE];       // Name of the node 
}; 
// The linked list of words 
static struct linkedList* theList = NULL; 

/** 
 * Add a name to the linked list. 
 */ 
static void addName(void) 
{ 
    printf("Enter word to add: "); 
    char line[NAME_SIZE];       // Input line 
    if (fgets(line, sizeof(line), stdin) == NULL) 
        return; 
    if (line[strlen(line)-1] == '\n') 
        line[strlen(line)-1] = '\0'; 
    // Get a new node. 
    struct linkedList* newNode = malloc(sizeof(*newNode)); 
    strncpy(newNode->name, line, sizeof(newNode->name)-1); 
    newNode->name[sizeof(newNode->name)-1] = '\0'; 
    newNode->next = theList; 
    theList = newNode; 
} 

/** 
 * Delete a word from the list. 
 */ 
static void deleteWord(void) 
{ 
    printf("Enter word to delete: "); 
    char line[NAME_SIZE];       // Input line 
    if (fgets(line, sizeof(line), stdin) == NULL) 
        return; 
    if (line[strlen(line)-1] == '\n') 
        line[strlen(line)-1] = '\0'; 
    struct linkedList* prevNode = NULL; // Pointer to the previous node 
    for (struct linkedList* curNode = theList; 
         curNode != NULL; 
         curNode = curNode->next) { 
        if (strcmp(curNode->name, line) == 0) { 
            if (prevNode == NULL) { 
                theList = curNode->next; 
            } else { 
                prevNode->next = curNode->next; 
            } 
            free(curNode); 
            curNode = NULL; 
            return; 
        } 
        prevNode = curNode; 
    } 
    printf("WARNING: Node not found %s\n", line); 
} 

/** 
 * Print the linked list. 
 */ 
static void printList(void) 
{ 
    // Loop over each node in the list. 
    for (const struct linkedList* curNode = theList; 
         curNode != NULL; 
         curNode = curNode->next) { 
        printf("%s, ", curNode->name); 
    } 
    printf("\n"); 
} 

int main() 
{ 
    while (true) { 
        printf("a-add, d-delete, p-print, q-quit: "); 
        char line[100]; // An input line 
        if (fgets(line, sizeof(line), stdin) == NULL) 
            break; 
        switch (line[0]) { 
            case 'a': 
                addName(); 
                break; 
            case 'd': 
                deleteWord(); 
                break; 
            case 'p': 
                printList(); 
                break; 
            case 'q': 
                exit(8); 
            default: 
                printf( 
                    "ERROR: Unknown command %c\n", line[0]); 
                break; 
        } 
    } 
}
动态内存问题
  1. 内存泄漏 :内存分配后未释放,会导致程序不断消耗内存,最终耗尽系统资源。
    • 示例:
{ 
    int* dynamicArray;    // A dynamic array 
    // Allocate 100 elements. 
    dynamicArray = malloc(sizeof(int) * 100); 
}
  1. 使用已释放的指针 :释放指针后继续使用,可能导致随机结果或覆盖随机内存。
    • 示例:
free(nodePtr); 
nextPtr = nodePtr->Next;   // Illegal
- 解决方法:释放指针后将其置为 `NULL`。
free(nodePtr); 
nodePtr = NULL; 
nextPtr = nodePtr->Next;   // Crashes the program
  1. 越界写入 :向结构末尾之外写入数据,可能破坏随机内存。
    • 示例:
int* theData;   // An array of data 
*theData = malloc(sizeof(*theData)*10); 
theData[0] = 0; 
theData[10] = 10; // Error
内存检测工具
  1. Valgrind :开源工具,可检测内存泄漏、越界写入、使用已释放指针和基于未初始化内存值做决策等问题。
    • 使用方法:编译程序后,运行 valgrind --leak-check=full ./program_name
  2. GCC 地址 sanitizer :编译时工具,可检测内存泄漏和越界写入。
    • 使用方法:编译代码时添加 -fsanitize=address 标志。
C 语言 I/O 系统
  1. printf 函数
    • 基本格式: printf(format-string, argument, ...)
    • 示例:
printf("Number: ->%d<-\n", 1234);   // Prints  ->1234<-
- 常用转换字符:
转换字符 参数类型 说明
%d 整数 char short int 类型作为参数时会提升为 int
%c 字符 实际接收整数参数并打印为字符
%o 整数 以八进制打印
%x 整数 以十六进制打印
%f 双精度浮点数 float 参数作为参数时会提升为 double
%l 长整数 long int 类型需要单独的转换
  1. 编写 ASCII 表程序
/** 
 * Print ASCII character table (only printable characters). 
 */ 
#include <stdio.h> 
int main() 
{ 
    for (char curChar = ' '; curChar <= '~'; ++curChar) { 
        printf("Char: %c Decimal %3d Hex 0x%02x Octal 0%03o\n", 
               curChar, curChar, curChar, curChar); 
    } 
    return (0); 
}
编程问题
  1. 修改单链表程序,使节点始终保持有序。
  2. 给定两个有序链表,编写函数返回公共节点列表。
  3. 将单链表程序改为双向链表,每个节点增加指向前一个节点的指针。
  4. 编写函数反转单链表的顺序。
  5. 编写函数去除链表中的重复节点。
流程图:单链表添加节点
graph TD;
    A[开始] --> B[输入要添加的单词];
    B --> C[创建新节点];
    C --> D[复制单词到新节点的 name 数组];
    D --> E[新节点的 next 指针指向当前链表头];
    E --> F[更新链表头指针指向新节点];
    F --> G[结束];
流程图:单链表删除节点
graph TD;
    A[开始] --> B[输入要删除的单词];
    B --> C[遍历链表查找目标节点];
    C --> D{找到目标节点?};
    D -- 是 --> E{目标节点是头节点?};
    E -- 是 --> F[更新链表头指针];
    E -- 否 --> G[前一个节点的 next 指针指向目标节点的下一个节点];
    F --> H[释放目标节点内存];
    G --> H;
    H --> I[结束];
    D -- 否 --> J[输出未找到节点警告];
    J --> I;

数据结构与动态内存管理:单链表与文件I/O

解决编程问题的思路
1. 修改单链表程序,使节点始终保持有序

为了让单链表中的节点始终有序,在添加节点时,需要遍历链表找到合适的插入位置。具体步骤如下:
1. 输入要添加的单词。
2. 创建新节点并复制单词到新节点的 name 数组。
3. 遍历链表,找到第一个比新节点单词大的节点位置。
4. 如果找到的位置是头节点,更新链表头指针;否则,将新节点插入到该位置。

static void addNameSorted(void) 
{ 
    printf("Enter word to add: "); 
    char line[NAME_SIZE];       // Input line 
    if (fgets(line, sizeof(line), stdin) == NULL) 
        return; 
    if (line[strlen(line)-1] == '\n') 
        line[strlen(line)-1] = '\0'; 
    // Get a new node. 
    struct linkedList* newNode = malloc(sizeof(*newNode)); 
    strncpy(newNode->name, line, sizeof(newNode->name)-1); 
    newNode->name[sizeof(newNode->name)-1] = '\0'; 

    struct linkedList* prevNode = NULL;
    struct linkedList* curNode = theList;

    while (curNode != NULL && strcmp(curNode->name, line) < 0) {
        prevNode = curNode;
        curNode = curNode->next;
    }

    if (prevNode == NULL) {
        newNode->next = theList;
        theList = newNode;
    } else {
        prevNode->next = newNode;
        newNode->next = curNode;
    }
}
2. 给定两个有序链表,编写函数返回公共节点列表

可以通过同时遍历两个链表,比较节点的 name 值来找出公共节点。具体步骤如下:
1. 初始化两个指针分别指向两个链表的头节点。
2. 比较两个指针指向的节点的 name 值:
- 如果相等,将该节点添加到结果链表中,并同时移动两个指针。
- 如果第一个链表的节点 name 值较小,移动第一个链表的指针。
- 如果第二个链表的节点 name 值较小,移动第二个链表的指针。
3. 重复步骤 2 直到其中一个链表遍历完。

struct linkedList* findCommonNodes(struct linkedList* list1, struct linkedList* list2) {
    struct linkedList* result = NULL;
    struct linkedList* tail = NULL;

    while (list1 != NULL && list2 != NULL) {
        int cmp = strcmp(list1->name, list2->name);
        if (cmp == 0) {
            struct linkedList* newNode = malloc(sizeof(*newNode));
            strncpy(newNode->name, list1->name, sizeof(newNode->name)-1);
            newNode->name[sizeof(newNode->name)-1] = '\0';
            newNode->next = NULL;

            if (result == NULL) {
                result = newNode;
                tail = newNode;
            } else {
                tail->next = newNode;
                tail = newNode;
            }
            list1 = list1->next;
            list2 = list2->next;
        } else if (cmp < 0) {
            list1 = list1->next;
        } else {
            list2 = list2->next;
        }
    }
    return result;
}
3. 将单链表程序改为双向链表,每个节点增加指向前一个节点的指针

双向链表的节点结构需要增加一个指向前一个节点的指针 prev 。同时,在添加、删除和打印节点时,需要相应地处理 prev 指针。

#define NAME_SIZE 20    // Max number of characters in a name 
/** 
 * A node in the doubly linked list 
 */ 
struct doublyLinkedList { 
    struct doublyLinkedList* next;    // Next node 
    struct doublyLinkedList* prev;    // Previous node 
    char name[NAME_SIZE];       // Name of the node 
};

// 添加节点
static void addNameDoubly(void) 
{ 
    printf("Enter word to add: "); 
    char line[NAME_SIZE];       // Input line 
    if (fgets(line, sizeof(line), stdin) == NULL) 
        return; 
    if (line[strlen(line)-1] == '\n') 
        line[strlen(line)-1] = '\0'; 
    // Get a new node. 
    struct doublyLinkedList* newNode = malloc(sizeof(*newNode)); 
    strncpy(newNode->name, line, sizeof(newNode->name)-1); 
    newNode->name[sizeof(newNode->name)-1] = '\0'; 

    if (theList == NULL) {
        newNode->next = NULL;
        newNode->prev = NULL;
        theList = newNode;
    } else {
        newNode->next = theList;
        theList->prev = newNode;
        newNode->prev = NULL;
        theList = newNode;
    }
}

// 删除节点
static void deleteWordDoubly(void) 
{ 
    printf("Enter word to delete: "); 
    char line[NAME_SIZE];       // Input line 
    if (fgets(line, sizeof(line), stdin) == NULL) 
        return; 
    if (line[strlen(line)-1] == '\n') 
        line[strlen(line)-1] = '\0'; 

    struct doublyLinkedList* curNode = theList;
    while (curNode != NULL) {
        if (strcmp(curNode->name, line) == 0) {
            if (curNode->prev == NULL) {
                theList = curNode->next;
                if (theList != NULL) {
                    theList->prev = NULL;
                }
            } else {
                curNode->prev->next = curNode->next;
                if (curNode->next != NULL) {
                    curNode->next->prev = curNode->prev;
                }
            }
            free(curNode);
            return;
        }
        curNode = curNode->next;
    }
    printf("WARNING: Node not found %s\n", line);
}

// 打印链表
static void printListDoubly(void) 
{ 
    struct doublyLinkedList* curNode = theList;
    while (curNode != NULL) {
        printf("%s, ", curNode->name);
        curNode = curNode->next;
    }
    printf("\n");
}
4. 编写函数反转单链表的顺序

可以通过遍历链表,依次改变节点的 next 指针方向来实现链表反转。具体步骤如下:
1. 初始化三个指针: prev 指向 NULL cur 指向链表头, next 指向 cur 的下一个节点。
2. 遍历链表,将 cur next 指针指向 prev ,然后更新 prev cur next 指针。
3. 最后更新链表头指针为 prev

void reverseList(struct linkedList** head) {
    struct linkedList* prev = NULL;
    struct linkedList* cur = *head;
    struct linkedList* next = NULL;

    while (cur != NULL) {
        next = cur->next;
        cur->next = prev;
        prev = cur;
        cur = next;
    }
    *head = prev;
}
5. 编写函数去除链表中的重复节点

可以通过遍历链表,使用一个临时指针记录已经出现过的节点,对于重复的节点进行删除。具体步骤如下:
1. 初始化一个指针 cur 指向链表头。
2. 遍历链表,对于每个节点,使用另一个指针 runner 从该节点的下一个节点开始遍历,删除所有与该节点 name 相同的节点。
3. 移动 cur 指针到下一个节点,重复步骤 2 直到链表结束。

void removeDuplicates(struct linkedList* head) {
    struct linkedList* cur = head;
    while (cur != NULL) {
        struct linkedList* runner = cur;
        while (runner->next != NULL) {
            if (strcmp(runner->next->name, cur->name) == 0) {
                struct linkedList* temp = runner->next;
                runner->next = runner->next->next;
                free(temp);
            } else {
                runner = runner->next;
            }
        }
        cur = cur->next;
    }
}
总结

本文介绍了单链表的基本操作,包括添加、打印和删除节点,同时讨论了动态内存管理中常见的问题,如内存泄漏、使用已释放的指针和越界写入,并介绍了相应的检测工具。此外,还介绍了 C 语言的 I/O 系统中的 printf 函数和编写 ASCII 表的程序。最后,针对一些编程问题给出了解决思路和代码示例。通过这些内容,我们可以更好地理解和应用单链表和动态内存管理,提高编程能力。

流程图:反转单链表
graph TD;
    A[开始] --> B[初始化 prev 为 NULL, cur 为链表头, next 为 NULL];
    B --> C{cur 不为 NULL?};
    C -- 是 --> D[next 指向 cur 的下一个节点];
    D --> E[cur 的 next 指针指向 prev];
    E --> F[prev 指向 cur];
    F --> G[cur 指向 next];
    G --> C;
    C -- 否 --> H[更新链表头指针为 prev];
    H --> I[结束];
流程图:去除链表重复节点
graph TD;
    A[开始] --> B[cur 指向链表头];
    B --> C{cur 不为 NULL?};
    C -- 是 --> D[runner 指向 cur];
    D --> E{runner 的下一个节点不为 NULL?};
    E -- 是 --> F{runner 的下一个节点的 name 与 cur 的 name 相同?};
    F -- 是 --> G[删除 runner 的下一个节点];
    G --> E;
    F -- 否 --> H[runner 指向 runner 的下一个节点];
    H --> E;
    E -- 否 --> I[cur 指向 cur 的下一个节点];
    I --> C;
    C -- 否 --> J[结束];
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值