数据结构day2

封面:

列队/栈

列队-栈的基本原理

计算机的两种存储方式,顺序存储(数组)和链式存储(链表)在之前的文章有详细介绍,可以去主页查看。

这部分讲解列队和栈的基本原理。

先说概念吧,其实队列和栈都是「操作受限」的数据结构。说它操作受限,主要是和基本的数组和链表相比,它们提供的 API 是不完整的

前面实现的数组和链表,增删查改的 API 都实现过了,你可以对任意一个索引元素进行增删查改,只要索引不越界,就可以进行任意操作。

但是对于队列和栈,它们的操作是受限的:队列只能在一端插入元素,另一端删除元素;栈只能在某一端插入和删除元素

形象地说,队列只允许在队尾插入元素,在队头删除元素,栈只允许在栈顶插入元素,从栈顶删除元素:

图中把栈竖着画,队列横着画,只是为了更形象,但实际上它们底层都是数组和链表实现的。

这两种输出结构的基本API如下(cpp):

# 创建列队
std::queue<int> myQueue;

// 入队
myQueue.push(10);

// 访问队首元素
std::cout << "Front element: " << myQueue.front() << std::endl;

// 出队
myQueue.pop();

// 再次访问队首元素
std::cout << "Front element after pop: " << myQueue.front() << std::endl;

// 遍历队列并输出
std::cout << "Queue elements: ";
while (!myQueue.empty()) {
    std::cout << myQueue.front() << " ";
    myQueue.pop();
}
    //创建栈
    std::stack<int> myStack;

    // 入栈
    myStack.push(10);
    myStack.push(20);
    myStack.push(30);

    // 访问栈顶元素
    std::cout << "Top element: " << myStack.top() << std::endl;

    // 出栈
    myStack.pop();

    // 再次访问栈顶元素
    std::cout << "Top element after pop: " << myStack.top() << std::endl;

    // 遍历栈并输出
    std::cout << "Stack elements: ";
    while (!myStack.empty()) {
        std::cout << myStack.top() << " ";
        myStack.pop(); // 每次迭代后出栈
    }
    std::cout << std::endl;

不同编程语言中,队列和栈提供的方法名称可能不一样,但每个方法的效果肯定是一样的。

用链表实现列队和栈

链表作为底层实现链表和栈是比较简单的,直接调用双链表的API即可。

template <typename T>
class MyLinkedStack {
private:
    std::list<T> list;

public:
    // 向栈顶加入元素,时间复杂度 O(1)
    void push(const T& element) {
        list.push_back(element);
    }

    // 从栈顶弹出元素,时间复杂度 O(1)
    T pop() {
        T element = list.back();
        list.pop_back();
        return element;
    }

    // 查看栈顶元素,时间复杂度 O(1)
    T& peek() {
        return list.back();
    }

    // 返回栈中的元素个数,时间复杂度 O(1)
    int size() {
        return list.size();
    }
};

用链表实现队列也是一样的,也直接调用双链表的 API 就可以了:

template <typename T>
class MyLinkedQueue {
private:
    std::list<T> list;

public:
    // 向队尾插入元素,时间复杂度 O(1)
    void push(const T& element) {
        list.push_back(element);
    }

    // 从队头删除元素,时间复杂度 O(1)
    T pop() {
        T element = list.front();
        list.pop_front();
        return element;
    }

    // 查看队头元素,时间复杂度 O(1)
    T& peek() {
        return list.front();
    }

    // 返回队列中的元素个数,时间复杂度 O(1)
    int size() {
        return list.size();
    }
};

环形数组技巧

数组是没有环形的说法的,但是我们可以通过逻辑将数组变成环形的,比如:

    int arr[] = {1, 2, 3, 4, 5};
    int length = sizeof(arr) / sizeof(arr[0]);
    int i = 0;

    // 模拟环形数组,这个循环永远不会结束
    while (true) {
        std::cout << arr[i] << std::endl;
        i = (i + 1) % length;
    }

这个技巧如何帮助我们在 O(1) 的时间在数组头部增删元素呢?

*核心原理

上面只是让大家对环形数组有一个直观地印象,环形数组的关键在于,它维护了两个指针 start 和 endstart 指向第一个有效元素的索引,end 指向最后一个有效元素的下一个位置索引。这样,当我们在数组头部添加或删除元素时,只需要移动 start 索引,而在数组尾部添加或删除元素时,只需要移动 end 索引。当 start, end 移动超出数组边界(< 0 或 >= arr.length)时,我们可以通过求模运算 % 让它们转一圈到数组头部或尾部继续工作,这样就实现了环形数组的效果。

代码实现:

#include <iostream>

template<typename T>
class CycleArray {
private:
    T *arr;
    int start;
    int end;
    int count;
    int size;

public:
    CycleArray() : CycleArray(1) {}

    CycleArray(int size) {
        this->size = size;
        this->arr = new T[size];
        this->start = 0;
        this->end = 0;
        this->count = 0;
    }

    ~CycleArray() {
        delete[] arr;
    }

    void resize(int newSize) {
        T *newArr = new T[newSize];
        for (int i = 0; i < count; ++i) {
            newArr[i] = arr[(start + i) % size];
        }
        delete[] arr;
        arr = newArr;
        start = 0;
        end = count;
        size = newSize;
    }

    void addFirst(T val) {
        if (isFull()) {
            resize(size * 2);
        }
        start = (start - 1 + size) % size;
        arr[start] = val;
        count++;
    }

    void removeFirst() {
        if (isEmpty()) {
            throw std::runtime_error("Array is empty");
        }
        arr[start] = T(); // Clear the element
        start = (start + 1) % size;
        count--;
        if (count > 0 && count == size / 4) {
            resize(size / 2);
        }
    }

    void addLast(T val) {
        if (isFull()) {
            resize(size * 2);
        }
        arr[end] = val;
        end = (end + 1) % size;
        count++;
    }

    void removeLast() {
        if (isEmpty()) {
            throw std::runtime_error("Array is empty");
        }
        end = (end - 1 + size) % size;
        arr[end] = T(); // Clear the element
        count--;
        if (count > 0 && count == size / 4) {
            resize(size / 2);
        }
    }

    T getFirst() {
        if (isEmpty()) {
            throw std::runtime_error("Array is empty");
        }
        return arr[start];
    }

    T getLast() {
        if (isEmpty()) {
            throw std::runtime_error("Array is empty");
        }
        return arr[(end - 1 + size) % size];
    }

    bool isFull() {
        return count == size;
    }

    int getSize() {
        return count;
    }

    bool isEmpty() {
        return count == 0;
    }
};

在数组增删头部元素的时间复杂度是 O(N),因为需要搬移元素。但是,如果我们使用环形数组,其实是可以实现在 O(1) 的时间复杂度内增删头部元素的。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值