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

双向链表在系统调度、游戏、文本编辑及组态方面的应用

在编程的奇妙世界里,数据结构就像是一把把神奇的钥匙(前面我们介绍过单向链表的基础了,这里我们更进一步),能帮我们打开解决各种问题的大门。今天,咱们就来聊聊其中一把特别的钥匙——双向链表。双向链表和普通链表相比,每个节点不仅有指向下一个节点的指针,还有指向前一个节点的指针,这就好比给它装上了“前后眼”,在很多场景下都能大显身手。接下来,我们通过双向链表在系统管理、游戏、文本编辑及组态软件中的应用来展示其强大,并给出示例代码。

文章目录

    • 1. 操作系统中的进程调度
      • 1.1 进程调度的需求
      • 1.2 双向链表的优势
      • 1.3 示例代码
        • 1.3.1 代码解释
    • 2. 双向链表在游戏开发中的应用
      • 2.1 游戏对象管理
        • 2.1.1 需求背景
        • 2.1.2 双向链表的优势
        • 2.1.3 示例代码
        • 2.1.4 代码解释
    • 3. 文本编辑器中的撤销与重做功能
      • 3.1 撤销与重做功能的原理
      • 3.2 双向链表的应用
      • 3.3 示例代码
        • 代码解释
    • 4. 缓存机制中的LRU缓存
      • 4.1 LRU缓存的原理
      • 4.2 双向链表与哈希表结合实现LRU缓存
      • 4.3 示例代码
        • 代码解释
    • 5. 双向链表在组态软件中的应用
      • 5.1 设备连接与监控
        • 5.1.1 需求背景
        • 5.1.2 双向链表的优势
        • 5.1.3 示例代码
        • 5.1.4 代码解释
          • `Device` 结构体
          • `DeviceList` 类
          • `addDevice(int id, const std::string& type, const std::string& address)` 方法
          • `removeDevice(int id)` 方法

1. 操作系统中的进程调度

1.1 进程调度的需求

同学们可以把操作系统想象成一个超大型的任务管理中心,这里有大量的进程需要被管理和调度。为了让整个系统高效运行,需要一个数据结构来存储进程的各种信息,并且要能方便地进行插入新进程、删除已完成进程以及对进程进行排序和查找等操作。这就好比一个繁忙的机场,需要一个高效的航班管理系统来安排航班的起降和调度。

1.2 双向链表的优势

双向链表在进程调度中就发挥了很大的作用。每个进程可以看作是双向链表中的一个节点,节点里存储着进程的关键信息,像进程 ID、优先级、运行状态等。由于双向链表在插入和删除节点时的时间复杂度是 O ( 1 ) O(1) O(1),这使得操作系统在创建新进程、结束进程或者调整进程优先级时能够快速完成。而且双向链表支持双向遍历,操作系统可以根据不同的调度算法,灵活地从前往后或者从后往前遍历链表,选择合适的进程来执行。

1.3 示例代码

#include <iostream>

// 定义进程结构体
struct Process {
    int pid;
    int priority;
    Process* prev;
    Process* next;
    Process(int id, int prio) : pid(id), priority(prio), prev(nullptr), next(nullptr) {}
};

// 定义双向链表类
class ProcessList {
private:
    Process* head;
public:
    ProcessList() : head(nullptr) {}

    // 插入新进程(按照优先级插入)
    void insertProcess(int pid, int priority) {
        Process* newProcess = new Process(pid, priority);
        if (!head || priority < head->priority) {
            newProcess->next = head;
            if (head) head->prev = newProcess;
            head = newProcess;
        } else {
            Process* current = head;
            while (current->next && priority >= current->next->priority) {
                current = current->next;
            }
            newProcess->next = current->next;
            newProcess->prev = current;
            if (current->next) current->next->prev = newProcess;
            current->next = newProcess;
        }
    }

    // 删除进程
    void deleteProcess(int pid) {
        Process* current = head;
        while (current && current->pid != pid) {
            current = current->next;
        }
        if (current) {
            if (current->prev) {
                current->prev->next = current->next;
            } else {
                head = current->next;
            }
            if (current->next) {
                current->next->prev = current->prev;
            }
            delete current;
        }
    }

    // 遍历链表
    void printProcesses() {
        Process* current = head;
        while (current) {
            std::cout << "PID: " << current->pid << ", Priority: " << current->priority << std::endl;
            current = current->next;
        }
    }
};
1.3.1 代码解释
  • 整体构思设计:我们要模拟操作系统中的进程调度,使用双向链表来存储进程信息。通过自定义的 Process 结构体表示进程节点,ProcessList 类来管理这些节点,实现进程的插入、删除和遍历功能。
  • Process 结构体
    • 包含进程的基本信息 pid(进程ID)和 priority(优先级)。
    • prevnext 指针分别指向前一个和后一个进程节点,用于构建双向链表。
    • 构造函数 Process(int id, int prio) 用于初始化进程节点。
  • ProcessList
    • insertProcess 方法
      • 功能:按照进程的优先级将新进程插入到合适的位置。
      • 实现思路:首先创建一个新的进程节点 newProcess。如果链表为空或者新进程的优先级比头节点的优先级高,则将新节点作为头节点。否则,遍历链表找到合适的插入位置,使得链表中的进程按照优先级从小到大排列。
    • deleteProcess 方法
      • 功能:根据进程ID删除指定的进程节点。
      • 实现思路:遍历链表找到具有指定 pid 的进程节点。如果找到,调整该节点前后节点的指针,将其从链表中移除,然后释放该节点的内存。
    • printProcesses 方法
      • 功能:遍历链表并打印每个进程的信息。
      • 实现思路:从头节点开始,依次访问每个节点,打印其 pidpriority 信息,然后移动到下一个节点,直到链表结束。

2. 双向链表在游戏开发中的应用

2.1 游戏对象管理

2.1.1 需求背景

想象一下,我们正在开发一款超酷的游戏,里面有各种各样的游戏对象,像角色、道具、怪物等等。游戏运行过程中,这些对象会不断地被创建、销毁,而且还需要对它们进行各种操作,比如移动、攻击、碰撞检测等。这时候,就需要一个高效的数据结构来管理这些游戏对象。

2.1.2 双向链表的优势

双向链表就非常适合这个场景。每个游戏对象可以作为链表中的一个节点,节点之间通过前后指针相连。这样,当我们需要添加新的游戏对象时,只需要在链表中插入一个新节点;当某个对象被销毁时,也能很方便地从链表中删除对应的节点。而且,双向链表支持双向遍历,我们可以根据需要从前往后或者从后往前遍历链表,这在进行碰撞检测等操作时非常有用。

2.1.3 示例代码
#include <iostream>
#include <string>

// 定义游戏对象结构体
struct GameObject {
    std::string name;
    int x;
    int y;
    GameObject* prev;
    GameObject* next;
    GameObject(const std::string& n, int _x, int _y) : name(n), x(_x), y(_y), prev(nullptr), next(nullptr) {}
};

// 定义游戏对象链表类
class GameObjectList {
private:
    GameObject* head;
public:
    GameObjectList() : head(nullptr) {}

    // 添加游戏对象
    void addGameObject(const std::string& name, int x, int y) {
        GameObject* newObj = new GameObject(name, x, y);
        if (!head) {
            head = newObj;
        } else {
            newObj->next = head;
            head->prev = newObj;
            head = newObj;
        }
    }

    // 删除游戏对象
    void deleteGameObject(const std::string& name) {
        GameObject* current = head;
        while (current) {
            if (current->name == name) {
                if (current->prev) {
                    current->prev->next = current->next;
                } else {
                    head = current->next;
                }
                if (current->next) {
                    current->next->prev = current->prev;
                }
                delete current;
                break;
            }
            current = current->next;
        }
    }

    // 遍历游戏对象
    void printGameObjects() {
        GameObject* current = head;
        while (current) {
            std::cout << "Name: " << current->name << ", Position: (" << current->x << ", " << current->y << ")" << std::endl;
            current = current->next;
        }
    }
};
2.1.4 代码解释
部分整体构思设计实现思路
GameObject 结构体为了表示游戏中的各种对象,我们定义了这个结构体。它包含了对象的名称和位置信息,以及用于构建双向链表的前后指针。成员变量 name 存储对象的名称,xy 存储对象的位置,prevnext 分别指向前一个和后一个节点。构造函数用于初始化这些成员。
GameObjectList这个类负责管理游戏对象链表,提供添加、删除和遍历游戏对象的功能。
addGameObject 方法首先创建一个新的游戏对象节点。如果链表为空,将新节点作为头节点;否则,将新节点插入到链表头部,更新前后指针。
deleteGameObject 方法遍历链表,找到名称匹配的游戏对象

3. 文本编辑器中的撤销与重做功能

3.1 撤销与重做功能的原理

在文本编辑器中,撤销与重做功能允许用户回退到之前的编辑状态或恢复之前撤销的操作。这需要一种数据结构来记录用户的每一步操作,并且能够方便地向前和向后追溯这些操作。

3.2 双向链表的应用

双向链表可以很好地实现这一功能。每一次用户的编辑操作(如插入字符、删除字符、修改文本等)都可以被记录为双向链表中的一个节点。节点中包含操作的详细信息,如操作类型、操作位置、操作内容等。当用户执行撤销操作时,程序可以沿着双向链表的前驱指针向前追溯到上一个操作状态,并还原相应的文本状态;当用户执行重做操作时,程序可以沿着双向链表的后继指针向后追溯到下一个操作状态,恢复之前撤销的操作。

3.3 示例代码

#include <iostream>
#include <string>

// 定义操作结构体
struct EditOperation {
    std::string type;
    int position;
    std::string content;
    EditOperation* prev;
    EditOperation* next;
    EditOperation(const std::string& t, int pos, const std::string& c)
        : type(t), position(pos), content(c), prev(nullptr), next(nullptr) {}
};

// 定义操作链表类
class EditHistory {
private:
    EditOperation* head;
    EditOperation* current;
public:
    EditHistory() : head(nullptr), current(nullptr) {}

    // 添加新操作
    void addOperation(const std::string& type, int position, const std::string& content) {
        EditOperation* newOp = new EditOperation(type, position, content);
        if (!head) {
            head = newOp;
            current = newOp;
        } else {
            newOp->prev = current;
            current->next = newOp;
            current = newOp;
        }
    }

    // 撤销操作
    void undo() {
        if (current && current->prev) {
            current = current->prev;
            // 这里根据操作类型实现具体的文本还原逻辑
            std::cout << "Undo: " << current->type << " at position " << current->position << std::endl;
        }
    }

    // 重做操作
    void redo() {
        if (current && current->next) {
            current = current->next;
            // 这里根据操作类型实现具体的文本恢复逻辑
            std::cout << "Redo: " << current->type << " at position " << current->position << std::endl;
        }
    }
};
代码解释
  • 整体构思设计:为了实现文本编辑器的撤销与重做功能,使用双向链表来记录用户的编辑操作。通过 EditOperation 结构体表示每个编辑操作节点,EditHistory 类来管理这些节点,实现操作的添加、撤销和重做功能。
  • EditOperation 结构体
    • 包含编辑操作的信息,如 type(操作类型,如插入、删除等)、position(操作位置)和 content(操作内容)。
    • prevnext 指针用于构建双向链表。
    • 构造函数 EditOperation(const std::string& t, int pos, const std::string& c) 用于初始化操作节点。
  • EditHistory
    • addOperation 方法
      • 功能:添加新的编辑操作到链表中。
      • 实现思路:创建一个新的操作节点 newOp。如果链表为空,将其作为头节点,并将当前操作指针 current 指向它。否则,将新节点添加到链表尾部,并更新 current 指针。
    • undo 方法
      • 功能:撤销上一次操作。
      • 实现思路:如果当前操作指针 current 不为空且有前一个操作节点,将 current 指针移动到前一个节点,并输出撤销操作的信息。在实际应用中,还需要根据操作类型实现具体的文本还原逻辑。
    • redo 方法
      • 功能:重做被撤销的操作。
      • 实现思路:如果当前操作指针 current 不为空且有下一个操作节点,将 current 指针移动到下一个节点,并输出重做操作的信息。同样,在实际应用中需要根据操作类型实现具体的文本恢复逻辑。

4. 缓存机制中的LRU缓存

4.1 LRU缓存的原理

LRU(Least Recently Used)缓存是一种常用的缓存淘汰策略,它的原理是当缓存满时,优先淘汰最近最少使用的元素。这需要一种数据结构来记录元素的访问顺序,并且能够快速地找到最近最少使用的元素。

4.2 双向链表与哈希表结合实现LRU缓存

双向链表与哈希表结合可以高效地实现LRU缓存。双向链表用于维护元素的访问顺序,最近访问的元素放在链表头部,最近最少使用的元素放在链表尾部。哈希表用于快速定位双向链表中的节点,这样在访问元素时,我们可以通过哈希表快速找到该元素在双向链表中的位置,然后将其移动到链表头部,表示该元素被最近访问过。当缓存满时,直接删除链表尾部的节点即可。

4.3 示例代码

#include <iostream>
#include <unordered_map>

// 定义缓存节点结构体
struct CacheNode {
    int key;
    int value;
    CacheNode* prev;
    CacheNode* next;
    CacheNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};

// 定义LRU缓存类
class LRUCache {
private:
    int capacity;
    std::unordered_map<int, CacheNode*> cache;
    CacheNode* head;
    CacheNode* tail;

public:
    LRUCache(int cap) : capacity(cap), head(nullptr), tail(nullptr) {}

    // 获取元素
    int get(int key) {
        if (cache.find(key)!= cache.end()) {
            CacheNode* node = cache[key];
            moveToFront(node);
            return node->value;
        }
        return -1;
    }

    // 插入元素
    void put(int key, int value) {
        if (cache.find(key)!= cache.end()) {
            CacheNode* node = cache[key];
            node->value = value;
            moveToFront(node);
        } else {
            CacheNode* newNode = new CacheNode(key, value);
            cache[key] = newNode;
            if (!head) {
                head = newNode;
                tail = newNode;
            } else {
                addToFront(newNode);
                if (cache.size() > capacity) {
                    removeTail();
                }
            }
        }
    }

private:
    // 将节点移动到链表头部
    void moveToFront(CacheNode* node) {
        if (node == head) return;
        if (node == tail) {
            tail = node->prev;
            tail->next = nullptr;
        } else {
            node->prev->next = node->next;
            node->next->prev = node->prev;
        }
        addToFront(node);
    }

    // 将节点添加到链表头部
    void addToFront(CacheNode* node) {
        node->next = head;
        node->prev = nullptr;
        if (head) head->prev = node;
        head = node;
        if (!tail) tail = head;
    }

    // 删除链表尾部节点
    void removeTail() {
        if (!tail) return;
        CacheNode* toDelete = tail;
        if (tail == head) {
            head = nullptr;
            tail = nullptr;
        } else {
            tail = tail->prev;
            tail->next = nullptr;
        }
        cache.erase(toDelete->key);
        delete toDelete;
    }
};
代码解释
  • 整体构思设计:为了实现 LRU 缓存,我们结合使用双向链表和哈希表。双向链表用于维护元素的访问顺序,哈希表用于快速查找元素。通过 CacheNode 结构体表示缓存节点,LRUCache 类来管理缓存的插入、获取和淘汰操作。
  • CacheNode 结构体
    • 包含缓存元素的 keyvalue
    • prevnext 指针用于构建双向链表。
    • 构造函数 CacheNode(int k, int v) 用于初始化缓存节点。
  • LRUCache
    • get 方法
      • 功能:根据 key 获取缓存中的元素。
      • 实现思路:首先在哈希表中查找 key。如果找到,将对应的节点移动到链表头部(表示该元素最近被访问),然后返回节点的值。如果未找到,返回 -1。
    • put 方法
      • 功能:插入或更新缓存元素。
      • 实现思路:如果 key 已经存在于缓存中,更新节点的值并将其移动到链表头部。如果 key 不存在,创建一个新的节点,将其添加到链表头部,并插入到哈希表中。如果缓存已满,删除链表尾部的节点(最近最少使用的元素)。
    • moveToFront 方法
      • 功能:将指定节点移动到链表头部。
      • 实现思路:如果节点已经是头节点,直接返回。如果节点是尾节点,更新尾节点指针。否则,调整节点前后节点的指针,将其从原位置移除,然后调用 addToFront 方法将其添加到链表头部。
    • addToFront 方法
      • 功能:将节点添加到链表头部。
      • 实现思路:更新节点的 next 指针指向原头节点,原头节点的 prev 指针指向该节点,然后将头节点指针更新为该节点。如果链表原本为空,同时更新尾节点指针。
    • removeTail 方法

5. 双向链表在组态软件中的应用

5.1 设备连接与监控

5.1.1 需求背景

组态软件在工业自动化领域可是个得力助手,它要负责监控和控制大量的工业设备。想象一下,一个大型工厂里有各种各样的机器设备,像传感器、电机、阀门等等,这些设备都需要和组态软件建立连接,软件要实时获取它们的状态信息,比如设备是否正常运行、当前的温度、压力等参数。同时,还得能随时对设备进行控制,比如启动、停止、调节参数等。这就需要一个高效的数据结构来管理这些设备的连接信息。

5.1.2 双向链表的优势

双向链表非常适合用于管理设备连接信息。每个设备可以看作是双向链表中的一个节点,节点里存储着设备的关键信息,如设备 ID、设备类型、通信地址、连接状态等。当有新设备接入系统时,我们可以方便地在链表中插入一个新节点;当某个设备出现故障或者断开连接时,也能快速地从链表中删除对应的节点。而且,双向链表支持双向遍历,我们可以根据需要从前往后或者从后往前遍历链表,这在进行设备状态查询、故障排查等操作时非常有用。

5.1.3 示例代码
#include <iostream>
#include <string>

// 定义设备结构体
struct Device {
    int deviceId;
    std::string deviceType;
    std::string communicationAddress;
    bool isConnected;
    Device* prev;
    Device* next;
    Device(int id, const std::string& type, const std::string& address)
        : deviceId(id), deviceType(type), communicationAddress(address), isConnected(false), prev(nullptr), next(nullptr) {}
};

// 定义设备链表类
class DeviceList {
private:
    Device* head;
public:
    DeviceList() : head(nullptr) {}

    // 添加设备
    void addDevice(int id, const std::string& type, const std::string& address) {
        Device* newDevice = new Device(id, type, address);
        if (!head) {
            head = newDevice;
        } else {
            newDevice->next = head;
            head->prev = newDevice;
            head = newDevice;
        }
    }

    // 删除设备
    void removeDevice(int id) {
        Device* current = head;
        while (current) {
            if (current->deviceId == id) {
                if (current->prev) {
                    current->prev->next = current->next;
                } else {
                    head = current->next;
                }
                if (current->next) {
                    current->next->prev = current->prev;
                }
                delete current;
                break;
            }
            current = current->next;
        }
    }

    // 遍历设备列表
    void printDevices() {
        Device* current = head;
        while (current) {
            std::cout << "Device ID: " << current->deviceId
                      << ", Type: " << current->deviceType
                      << ", Address: " << current->communicationAddress
                      << ", Connected: " << (current->isConnected ? "Yes" : "No") << std::endl;
            current = current->next;
        }
    }

    // 查找设备
    Device* findDevice(int id) {
        Device* current = head;
        while (current) {
            if (current->deviceId == id) {
                return current;
            }
            current = current->next;
        }
        return nullptr;
    }
};
5.1.4 代码解释
Device 结构体
  • 整体构思设计:在这个组态软件设备管理的场景中,我们需要一个数据结构来代表每一个具体的工业设备。Device 结构体就承担了这个角色,它将设备的关键属性和用于构建双向链表的指针封装在一起,使得每个设备节点可以方便地连接到链表中,便于对设备进行统一管理。
  • 各成员变量说明
    • int deviceId:这是设备的唯一标识符,就像设备的身份证号码一样,在整个系统中每个设备的 deviceId 都是独一无二的。通过这个 ID,我们可以准确地定位和区分不同的设备,方便进行设备的查找、删除等操作。
    • std::string deviceType:用于记录设备的类型,比如传感器、电机、阀门等。不同类型的设备可能具有不同的功能和操作方式,这个属性可以帮助我们在处理设备时进行分类和区分。
    • std::string communicationAddress:表示设备的通信地址,组态软件通过这个地址与设备进行通信,获取设备的状态信息或者向设备发送控制指令。它可以是 IP 地址、串口地址等具体的通信标识。
    • bool isConnected:用于标记设备的连接状态,true 表示设备已连接到系统,false 表示设备未连接。通过这个状态,我们可以快速了解设备的工作情况,及时发现设备连接异常。
    • Device* prevDevice* next:这两个指针是构建双向链表的关键。prev 指针指向前一个设备节点,next 指针指向后一个设备节点。通过这两个指针,我们可以在链表中方便地进行双向遍历,从一个节点快速访问到它的前一个或后一个节点。
  • 构造函数 Device(int id, const std::string& type, const std::string& address):该构造函数用于初始化 Device 结构体的各个成员变量。当创建一个新的设备节点时,我们需要传入设备的 ID、类型和通信地址,构造函数会将这些值赋给相应的成员变量,并将 isConnected 初始化为 false,表示设备初始状态为未连接,同时将 prevnext 指针初始化为 nullptr,表示该节点暂时没有前后连接的节点。
DeviceList
  • 整体构思设计:这个类是对设备链表的管理类,它封装了对设备链表的各种操作,如添加设备、删除设备、遍历设备列表和查找设备等。通过这个类,我们可以方便地对整个设备链表进行统一管理,而不需要直接操作链表节点的指针,提高了代码的可读性和可维护性。
  • 成员变量 Device* head:这是一个指向链表头节点的指针,它是整个设备链表的入口。通过 head 指针,我们可以访问链表中的第一个设备节点,进而遍历整个链表。当链表为空时,head 指针为 nullptr
  • 构造函数 DeviceList():在创建 DeviceList 对象时,该构造函数会将 head 指针初始化为 nullptr,表示此时设备链表为空,没有任何设备节点。
addDevice(int id, const std::string& type, const std::string& address) 方法
  • 功能设计:该方法用于向设备链表中添加一个新的设备节点。
  • 实现思路
    1. 首先,根据传入的设备 ID、类型和通信地址创建一个新的 Device 对象 newDevice
    2. 然后判断链表是否为空,即检查 head 指针是否为 nullptr。如果链表为空,说明这是第一个添加的设备节点,直接将 head 指针指向 newDevice
    3. 如果链表不为空,将新节点插入到链表的头部。具体操作是将 newDevicenext 指针指向当前的头节点 head,同时将当前头节点 headprev 指针指向 newDevice,最后更新 head 指针为 newDevice,这样新节点就成为了新的头节点。
removeDevice(int id) 方法
  • 功能设计:该方法用于从设备链表中删除指定 ID 的设备节点。
  • 实现思路
    1. 初始化一个指针 current 指向链表的头节点 head,从链表的第一个节点开始遍历。

http://www.kler.cn/a/522498.html

相关文章:

  • 【JavaWeb06】Tomcat基础入门:架构理解与基本配置指南
  • 免杀国内主流杀软的恶意样本分析
  • Excel - Binary和Text两种Compare方法
  • 把本地搭建的hexo博客部署到自己的服务器上
  • CMake常用命令指南(CMakeList.txt)
  • Mac m1,m2,m3芯片使用nvm安装node14报错
  • 【llm对话系统】LLM 大模型Prompt 怎么写?
  • 2025多目标优化创新路径汇总
  • 快速生成2D卡通人物的AI工具:开启Live2D角色创作的新时代
  • SuperAGI - 构建、管理和运行 AI Agent
  • CAN总线数据采集与分析
  • React第二十六章(createPortal)
  • 学习率衰减策略
  • 常见字符串相关题目
  • Linux之内存管理前世今生(一)
  • 【公式】卢布贬值风险:义乌到俄罗斯贸易的汇率陷阱
  • 图漾相机搭配VisionPro使用简易教程
  • 异或哈希总结
  • 【信息系统项目管理师-选择真题】2013上半年综合知识答案和详解
  • ubuntu x64下交叉编译ffmpeg到目标架构为aarch架构的系统
  • 基于STM32的阿里云智能农业大棚
  • 「 机器人 」扑翼飞行器数据驱动建模浅谈
  • Reinforcement learning 强化学习
  • 【Elasticsearch】脚本查询需要字段时使用的docValues结构吗?
  • CSS中的响应式布局初识
  • 【Super Tilemap Editor使用详解】(十六):高级主题:深入理解 Super Tilemap Editor