数据结构-单向循环链表

单向环形链表是一种特殊的数据结构,它在单链表的基础上进行了扩展,使得链表的最后一个节点指向链表的第一个节点,从而形成一个环状结构。以下是关于单向环形链表的主要知识点归纳:

一、定义与结构

  1. 定义:单向环形链表是指在单链表的基础上,最后一个节点的指针域指向链表的第一个节点,从而构成一个闭合的环。
  2. 结构:每个节点包含一个数据域和一个指针域。数据域用于存储数据,指针域用于指向下一个节点。在单向环形链表中,最后一个节点的指针域指向第一个节点,形成环状结构。

二、特点与操作

  1. 特点:
    • 环形结构:链表的尾部连接到头部,形成环状。
    • 遍历方式:可以通过任意一个节点作为起点开始遍历整个链表,最终会回到起点。
    • 无需处理边界情况:在环形链表中,无需特殊处理链表的头部和尾部,因为它们是相连的。
  2. 操作:
    • 插入节点:可以在链表的任意位置插入新节点,需要调整相应节点的指针域以确保链表的完整性。
    • 删除节点:可以删除链表中的任意节点,同样需要调整相邻节点的指针域以保持链表的连贯性。
    • 遍历链表:可以从任意节点开始,沿着指针域依次访问链表中的每个节点,直到回到起始节点。

三、应用场景与实例

  1. 约瑟夫问题(Josephus Problem):这是一个经典的数学问题,描述了一群人围坐在一圈,每次数到某个数字的人出列,直到只剩下最后一个人。单向环形链表可以很好地模拟这个过程,通过每次删除指定位置的节点来解决问题。
  2. 环形缓冲区:单向环形链表可用于实现环形缓冲区,如音频、视频等数据的输入和输出。数据可以循环写入和读取,提高数据处理效率。
  3. 循环队列:单向环形链表还可用于实现循环队列,适用于操作系统中的任务调度、缓冲区管理等场景。循环队列可以有效地管理有限的资源,实现高效的任务调度和数据处理。

四、代码实现

在代码实现方面,单向环形链表的操作与单向链表类似,但需要考虑环形结构的特点。例如,在创建单向环形链表时,需要确保最后一个节点的指针域指向第一个节点;在遍历链表时,需要设置一个标志来判断是否已遍历完整个链表等。具体的代码实现会根据具体的编程语言和需求而有所不同。

综上所述,单向环形链表是一种具有特殊结构的数据类型,它在许多场景中具有广泛的应用价值。通过掌握其定义、结构、特点、应用场景以及代码实现等方面的知识点,可以更好地理解和应用这种数据结构。

五、约瑟夫问题

约瑟夫问题描述了一个特定的场景:N个人围成一圈,从第一个人开始报数,每次数到M的人将被淘汰,然后从下一个人重新开始报数。这个过程持续进行,直到只剩下一个人为止。该问题的核心在于,给定N和M,确定最初的站位以避免被淘汰,或者预测被淘汰的顺序。

约瑟夫问题有多种求解方法,包括使用链表或数组来模拟整个过程。一种常见的思路是使用布尔型数组来标记每个人的状态,然后模拟整个过程,直到只剩下一个人为止。这种方法的时间复杂度较高,为O(NM),当N和M非常大时,可能不太适用。

class Node {  
    int data;  
    Node next;  
  
    public Node(int data) {  
        this.data = data;  
    }  
}  
  
public class JosephusProblem {  
    public static void main(String[] args) {  
        int n = 10; // 总人数  
        int m = 3;  // 报数的数字  
          
        Node head = createCircularLinkedList(n);  
        solveJosephus(head, n, m);  
    }  
  
    // 创建单向循环链表  
    private static Node createCircularLinkedList(int n) {  
        Node head = new Node(1);  
        Node current = head;  
        for (int i = 2; i <= n; i++) {  
            Node newNode = new Node(i);  
            current.next = newNode;  
            current = newNode;  
        }  
        current.next = head; // 形成环  
        return head;  
    }  
  
    // 解决约瑟夫问题  
    private static void solveJosephus(Node head, int n, int m) {  
        Node prev = null;  
        Node current = head;  
          
        // 用于记录已经报数的次数  
        int count = 0;  
          
        while (n > 0) {  
            // 报数  
            count++;  
              
            // 如果报数达到m,则删除当前节点  
            if (count == m) {  
                System.out.println("被淘汰的人是:" + current.data);  
                  
                // 删除当前节点  
                if (prev == null) { // 如果当前节点是头节点  
                    head = current.next; // 更新头节点  
                    prev = head; // prev指向新的头节点  
                    while (prev.next != current) { // 找到current的前一个节点  
                        prev = prev.next;  
                    }  
                }  
                prev.next = current.next;  
                  
                // 清理当前节点并移动  
                Node temp = current;  
                current = current.next;  
                temp.next = null; // 帮助垃圾回收  
                  
                n--; // 人数减少  
                count = 0; // 重置报数  
            } else {  
                prev = current;  
                current = current.next;  
                  
                // 如果只剩下一个节点,则直接结束  
                if (current == head && n == 1) {  
                    break;  
                }  
            }  
        }  
          
        // 输出最后剩下的人  
        System.out.println("最后剩下的人是:" + current.data);  
    }  
}

六、环形缓冲区

环形缓冲区(Circular Buffer)或称为循环缓冲区、环形队列,是一种在内存中形成环形存储空间的数据结构。它具有以下特点和应用:

  • 环形缓冲区的特点:
  1. 环形结构:环形缓冲区的特点是其终点和起点相连,形成一个闭环结构。当缓冲区写满时,新数据可以覆盖旧数据,从而实现数据的循环利用。

  2. 内存利用率高:由于能够循环利用缓冲区空间,环形缓冲区避免了频繁的内存分配和释放,从而提高了内存利用率。

  3. 读写效率高:环形缓冲区的读写操作主要涉及指针的移动,无需进行元素的搬移或内存复制。读写指针以固定步长移动,这使得读写效率较高,特别适用于实时数据处理。

  4. 简单且高效:相比其他复杂的数据结构,环形缓冲区的实现较为简单,且性能较高。它不需要复杂的内存管理或链表操作。

  • 环形缓冲区的应用场合:
  1. 数据流处理:环形缓冲区常用于处理连续的数据流,如音频、视频、传感器数据等。通过循环利用缓冲区的空间,它可以实现高效的数据流存储和快速的读取处理。

  2. 缓冲数据传输:在生产者和消费者之间的数据传输场景中,环形缓冲区可以平衡两者的速度差异,避免数据丢失或阻塞。

  3. 数据采样和循环记录:适用于需要采样和记录连续数据的应用,如嵌入式系统中的数据采集、实时监控系统中的历史数据记录等。

  • 环形缓冲区的实现:

环形缓冲区的实现通常涉及两个主要指针:读指针和写指针。读指针用于提取数据,而写指针用于写入数据。此外,还需要记录缓冲区的大小和当前数据元素的数量。通过合理管理这些指针和计数信息,可以实现有效的数据读写和同步,从而避免数据溢出和读写冲突的问题。

综上所述,环形缓冲区是一种高效、简单且内存利用率高的数据结构,特别适用于处理流数据和实现数据缓存等场景。

class Node<T> {  
    T data;  
    Node<T> next;  
  
    public Node(T data) {  
        this.data = data;  
    }  
}  
  
public class CircularBuffer<T> {  
    private Node<T> head;  
    private Node<T> tail;  
    private int capacity;  
    private int size;  
  
    public CircularBuffer(int capacity) {  
        if (capacity <= 0) {  
            throw new IllegalArgumentException("Capacity must be positive.");  
        }  
        this.capacity = capacity;  
        this.size = 0;  
        // 初始化一个空的环形缓冲区,头尾节点指向一个哨兵节点(不存储实际数据)  
        head = new Node<>(null);  
        tail = head;  
    }  
  
    public boolean isFull() {  
        return size == capacity;  
    }  
  
    public boolean isEmpty() {  
        return size == 0;  
    }  
  
    public void enqueue(T data) {  
        if (isFull()) {  
            throw new IllegalStateException("Buffer is full.");  
        }  
        Node<T> newNode = new Node<>(data);  
        tail.next = newNode; // 将新节点连接到尾部  
        tail = newNode; // 更新尾指针  
        tail.next = head.next; // 形成环,新节点的下一个节点指向头节点的下一个节点(哨兵节点的下一个节点)  
        size++;  
    }  
  
    public T dequeue() {  
        if (isEmpty()) {  
            throw new IllegalStateException("Buffer is empty.");  
        }  
        Node<T> firstNode = head.next; // 头节点的下一个节点是第一个有效节点  
        T data = firstNode.data;  
        head.next = firstNode.next; // 移除第一个节点  
        if (tail == firstNode) { // 如果移除的是最后一个节点,需要更新尾指针  
            tail = head;  
        }  
        size--;  
        return data;  
    }  
  
    public static void main(String[] args) {  
        CircularBuffer<Integer> buffer = new CircularBuffer<>(5);  
          
        // 入队几个元素  
        buffer.enqueue(1);  
        buffer.enqueue(2);  
        buffer.enqueue(3);  
          
        // 出队并打印元素  
        System.out.println(buffer.dequeue()); // 输出: 1  
        System.out.println(buffer.dequeue()); // 输出: 2  
          
        // 再次入队几个元素  
        buffer.enqueue(4);  
        buffer.enqueue(5);  
        buffer.enqueue(6); // 这个会覆盖之前出队的3的位置  
          
        // 再次出队并打印元素  
        System.out.println(buffer.dequeue()); // 输出: 3(已被覆盖)或 4(如果3未被覆盖)  
        System.out.println(buffer.dequeue()); // 输出: 下一个元素  
          
        // 可以继续操作...  
    }  
}

注意:这个实现中,我加入了一个哨兵节点(也称为虚拟头节点或哑节点),它简化了某些操作,特别是当缓冲区为空或满时的边界情况处理。此外,为了保持实现的简洁性,这个环形缓冲区在满时不会自动扩容,且在空时不会自动缩容。在实际应用中,可以根据需求进行相应的调整。

在这个实现中,enqueue 方法用于向缓冲区添加元素,而 dequeue 方法用于从缓冲区中移除并返回元素。如果尝试在缓冲区已满时添加元素或在缓冲区为空时移除元素,将抛出异常。这个简单的环形缓冲区可以作为更复杂数据结构的基础,或者用于需要高效数据读写的场景。

七、循环队列

循环队列是一种特殊的线性数据结构,它利用数组实现队列的操作,并通过两个指针(front 和 rear)来追踪队列的首部和尾部。循环队列的关键特性是当尾部指针到达数组的末端时,它会循环回到数组的开始位置,从而实现“循环”的效果。这种方式有效地利用了数组空间,避免了普通队列中可能的空间浪费。

以下是一个简单的循环队列的 Java 实现:

public class CircularQueue {  
    private int[] queue;  
    private int front; // 队头指针  
    private int rear;  // 队尾指针  
    private int capacity; // 队列容量  
  
    public CircularQueue(int k) {  
        this.capacity = k + 1; // 多一个空间用于判断队列满  
        queue = new int[this.capacity];  
        front = 0;  
        rear = 0;  
    }  
  
    // 插入元素到队尾  
    public boolean enqueue(int value) {  
        if (isFull()) {  
            return false;  
        }  
        queue[rear] = value;  
        rear = (rear + 1) % capacity;  
        return true;  
    }  
  
    // 从队头删除元素  
    public boolean dequeue() {  
        if (isEmpty()) {  
            return false;  
        }  
        front = (front + 1) % capacity;  
        return true;  
    }  
  
    // 从队头获取元素  
    public int front() {  
        if (isEmpty()) {  
            return -1; // 或者抛出异常  
        }  
        return queue[front];  
    }  
  
    // 获取队尾元素  
    public int rear() {  
        if (isEmpty()) {  
            return -1; // 或者抛出异常  
        }  
        return queue[(rear - 1 + capacity) % capacity]; // 处理rear为0的情况  
    }  
  
    // 检查队列是否为空  
    public boolean isEmpty() {  
        return front == rear;  
    }  
  
    // 检查队列是否已满  
    public boolean isFull() {  
        return (rear + 1) % capacity == front;  
    }  
}

在这个实现中,enqueue 方法用于在队尾插入元素,dequeue 方法用于从队头删除元素,front 方法返回队头元素,而 rear 方法返回队尾元素。我们还定义了 isEmpty 和 isFull 方法来检查队列是否为空或已满。

请注意,为了实现循环,我们在更新 rear 和 front 指针时使用了模运算(% capacity)。这确保了当指针到达数组末端时,它会回到数组的开始位置。

此外,为了区分空队列和满队列的情况(因为在这两种情况下 front 和 rear 都是相等的),我们为队列分配了比实际所需多一个的空间。这样,当队列满时,rear 指针会指向 front 指针前面的一个位置,而不是与 front 重合。这是循环队列设计中常见的一个技巧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没出过地球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值