简易STL实现 | PriorityQueue 的实现

1、priority_queue 的底层是堆,标准库中 直接使用 std::make_heap, std::push_heap, std::pop_heap 来实现 priority_queue

2、std::make_heap、std::push_heap 和 std::pop_heap 这三个函数 用于 处理堆数据结构(Heap)。堆 是一种特殊的完全二叉树,通常用于 实现优先队列,最常见的是 最大堆(每个节点的值 都大于或等于其子节点的值)或 最小堆(每个节点的值都小于或等于其子节点的值)

1)std::make_heap
功能:将一个范围内的元素转化为堆结构。
原型:template <class RandomIt> void make_heap(RandomIt first, RandomIt last);
解释:这个函数将一段范围 [first, last) 内的元素重新排列,使其符合堆的性质(默认情况下 是最大堆)

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    
    
    std::vector<int> v = {
    
    3, 1, 4, 1, 5, 9, 2};
    std::make_heap(v.begin(), v.end());
    for (int n : v) {
    
    
        std::cout << n << ' '; // 输出最大堆
    }
}

2)std::push_heap
功能:将一个新元素 插入到 已经是堆的数据结构中,并调整 使其继续保持堆的性质
原型:template <class RandomIt> void push_heap(RandomIt first, RandomIt last);
解释:假设 [first, last-1) 范围已经是一个堆,push_heap 将范围内 最后一个元素(即 *(last-1))插入到正确的位置以维持堆的性质

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    
    
    std::vector<int> v = {
    
    3, 1, 4, 1, 5, 9};
    std::make_heap(v.begin(), v.end()); // 先将其变为堆
    v.push_back(7);  // 添加新元素
    std::push_heap(v.begin(), v.end()); // 调整堆结构
    for (int n : v) {
    
    
        std::cout << n << ' '; // 输出调整后的堆
    }
}

3)std::pop_heap
功能:将堆的根元素(最大元素或最小元素)移到范围的末尾,同时 调整剩余元素,使它们仍保持堆的性质。
原型:template <class RandomIt> void pop_heap(RandomIt first, RandomIt last);
解释:pop_heap 将堆的第一个元素(根元素)与最后一个元素交换,然后调整 [first, last-1) 范围内的元素,使其依然满足堆的性质。此时,last-1 指向原来的堆顶元素

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    
    
    std::vector<int> v = {
    
    3, 1, 4, 1, 5, 9, 2};
    std::make_heap(v.begin(), v.end()); // 先将其变为堆
    std::pop_heap(v.begin(), v.end());  // 移动最大元素到末尾
    v.pop_back(); // 移除最大元素
    for (int n : v) {
    
    
        std::cout << n << ' '; // 输出剩余的堆
    }
}

1、堆的插入和删除

插入操作: 插入新元素时,新元素 首先被放置在 树的最后一个位置,以保持 完全二叉树的结构。然后,该元素 会通过一个称为“上浮”(或“堆化”)的过程,与其父节点比较 并交换位置(如果在最大堆中 新元素比父节点大,或在最小堆中 新元素比父节点小)。这个过程重复进行,直到 新元素到达一个位置,它不再比父节点大(或小,取决于 是最大堆还是最小堆),或者 它已经到达了树的顶部

在这里插入图片描述

删除操作: 在堆中,删除操作 通常指的是删除根节点,即最大元素 或 最小元素。删除后,堆的结构性质 必须得到维护。这通常 通过 将最后一个元素移到根节点的位置来完成,接着执行“下沉”(或“堆化”)过程,该元素 会与其子节点比较 并根据需要 与较大(或较小)的子节点交换位置。这个过程持续进行,直到 该元素位于正确的位置,或者 它已经到达了树的底部

在这里插入图片描述

构建堆: 从无序数组 构建堆的过程 称为堆化(Heapify)。这可以通过 从 最后一个非叶子节点开始,对每个节点执行下沉操作 来完成。在数组中,给定索引为 i 的元素,其左子节点的索引为 2 * i + 1,右子节点的索引为 2 * i + 2,父节点的索引为 (i - 1) / 2

2、利用标准库堆实现

#include <iostream>
#include <vector>
#include <algorithm> // 用于 std::make_heap, std::push_heap, std::pop_heap
#include <stdexcept>
#include <sstream> // 为了使用 istringstream

template <typename T, typename Container = std::vector<T>>
class PriorityQueue {
    
    
    Container data; // 始终保持堆的性质

public:
    PriorityQueue() {
    
    }

    PriorityQueue(const Container& c) : data(c) {
    
    
        std::make_heap(data.begin(), data.end());
    }

    void push(const T& val) {
    
    
        data.push_back(val);
        std::push_heap(data.begin(), data.end());
    }

    void pop() {
    
    
        if (!data.empty()) {
    
    
            std::pop_heap(data.begin(), data.end());
            data.pop_back();
        }
        else
            throw std::runtime_error("PriorityQueue is empty. ");
    }

    T& top() {
    
    
        if (!data.empty()) {
    
    
            return data.front(); // 顶部在数据结构的最前面
        }
        else
            throw std::runtime_error("PriorityQueue is empty. ");
    }

    bool empty() const {
    
    
        return data.empty();
    }

    size_t size() const {
    
    
        return data.size();
    }
};

int main() {
    
    
    // 使用 std::vector 作为底层容器
    PriorityQueue<int, std::vector<int>> myPriorityQueue;

    int N;
    std::cin >> N;
    getchar();

    std::string line;
    for (int i = 0; i < N; i++) {
    
    
        std::getline(std::cin, line);
        std::istringstream iss(line);
        std::string command;
        iss >> command;

        int element;

        if (command == "push") {
    
    
            iss >> element;
            myPriorityQueue.push(element);
        }

        if (command == "pop") {
    
    
            try {
    
    
                myPriorityQueue.pop();
            }
            catch (const std::runtime_error& e) {
    
    
                // 不做任何处理,无输出
                continue;
            }
        }

        if (command == "top") {
    
    
            try {
    
    
                std::cout << myPriorityQueue.top() << std::endl;
            }
            catch (const std::runtime_error& e) {
    
    
                std::cout << "null" << std::endl; //如果 priority_queue 为空,则输出 null
            }
        }

        if (command == "size") {
    
    
            std::cout << myPriorityQueue.size() << std::endl;
        }

        if (command == "empty") {
    
    
            std::cout << (myPriorityQueue.empty() ? "true" : "false") << std::endl;
        }
    }
    return 0;
}

3、利用自己实现堆实现

#include <iostream>
#include <vector>
#include <stdexcept> // std::runtime_error
#include <sstream> // 为了使用 istringstream
#include <utility>  // std::swap 所在的头文件

template <typename T, typename Container = std::vector<T>>
class MaxHeap {
    
    
    Container data;

    // 下标
    int parent(int i) {
    
    
        return (i - 1) / 2;
    }

    int leftChild(int i) {
    
    
        return 2 * i + 1;
    }

    int rightChild(int i) {
    
    
        return 2 * i + 2;
    }

    void heapify_down(int i) {
    
     // 结点不断下沉,循环不是递归
        while (true) {
    
    
            int largest = i;
            int left = leftChild(i);
            int right = rightChild(i);
            // 得出三者的最大值
            if (left < data.size() && data[left] > data[largest]) {
    
    
                largest = left;
            }
            if (right < data.size() && data[right] > data[largest]) {
    
    
                largest = right;
            }
            if (largest != i) {
    
    
                std::swap(data[largest], data[i]);
                i = largest;
            }
            else {
    
    
                return; // 结束了
            }
        }
    }

    void heapify_up(int i) {
    
     // 结点不断往上升,同样是循环不是递归
        int par = parent(i);
        while (par >= 0 && data[par] < data[i]) {
    
    
            std::swap(data[par], data[i]);
            i = par;
            par = parent(i);
        }
    }

public:
    MaxHeap() {
    
    }

    MaxHeap(const Container& c) : data(c) {
    
    
        int size = data.size();
        for (int i = (size / 2) - 1; i >= 0; i--) {
    
     // 叶子结点显然不需要下沉
            heapify_down(i);
        }
    }

    // 接受迭代器的版本
    MaxHeap(typename Container::iterator begin, typename Container::iterator end) {
    
    
        data = Container(begin, end);
        int size = data.size();
        for (int i = (size / 2) - 1; i >= 0; i--) {
    
     // 叶子结点显然不需要下沉
            heapify_down(i);
        }
    }
    void push(const T &val) {
    
    
        data.push_back(val);
        heapify_up(data.size() - 1);
    }

    void pop() {
    
    
        if (data.size() == 0)
            throw std::out_of_range("Heap is empty. ");
        data[0] = data.back(); // 将最后一个节点换到根位置,删除最后一个结点,再下降
        data.pop_back();
        heapify_down(0);
    }

    bool isEmpty() const {
    
    
        return data.size() == 0;
    }

    T& top() {
    
    
        return data[0];
    }

    size_t size() const {
    
    
        return data.size();
    }
};

template <typename T, typename Container = std::vector<T>>
class PriorityQueue {
    
    
    // Container data;
    MaxHeap<T> mh;

public:
    PriorityQueue() {
    
    }

    PriorityQueue(const Container& c) : mh(c) {
    
    }

    void push(const T& val) {
    
    
        mh.push(val);
    }

    void pop() {
    
    
        if (!mh.isEmpty()) {
    
    
            mh.pop();
        }
        else
            throw std::runtime_error("PriorityQueue is empty. ");
    }

    T& top() {
    
    
        if (!mh.isEmpty()) {
    
    
            return mh.top(); // 顶部在数据结构的最前面
        }
        else
            throw std::runtime_error("PriorityQueue is empty. ");
    }

    bool empty() const {
    
    
        return mh.isEmpty();
    }

    size_t size() const {
    
    
        return mh.size();
    }
};

int main() {
    
    
    // 使用 std::vector 作为底层容器
    PriorityQueue<int, std::vector<int>> myPriorityQueue;

    int N;
    std::cin >> N;
    getchar();

    std::string line;
    for (int i = 0; i < N; i++) {
    
    
        std::getline(std::cin, line);
        std::istringstream iss(line);
        std::string command;
        iss >> command;

        int element;

        if (command == "push") {
    
    
            iss >> element;
            myPriorityQueue.push(element);
        }

        if (command == "pop") {
    
    
            try {
    
    
                myPriorityQueue.pop();
            }
            catch (const std::runtime_error& e) {
    
    
                // 不做任何处理,无输出
                continue;
            }
        }

        if (command == "top") {
    
    
            try {
    
    
                std::cout << myPriorityQueue.top() << std::endl;
            }
            catch (const std::runtime_error& e) {
    
    
                std::cout << "null" << std::endl; //如果 priority_queue 为空,则输出 null
            }
        }

        if (command == "size") {
    
    
            std::cout << myPriorityQueue.size() << std::endl;
        }

        if (command == "empty") {
    
    
            std::cout << (myPriorityQueue.empty() ? "true" : "false") << std::endl;
        }
    }
    return 0;
}

1、构造堆的过程:
为什么从 (size / 2) - 1 开始:表示 从最后一个非叶子节点开始,向上依次 对每个节点进行堆调整。这是因为 叶子节点本身已经是 符合堆的结构,无需调整

2、这里 默认构造函数 怎么自己构造 Container

template <typename T, typename Container = std::vector<T>>
class PriorityQueue {
    
    
    Container data;
    
public:
    PriorityQueue() {
    
    }
};

Container data; 会自动调用 Container 的默认构造函数
C++ 会自动调用 Container(即 std::vector)的默认构造函数 来初始化 data,因此 data 也会被默认构造为一个空容器

3、什么情况下 默认构造函数会被自动调用
1)定义类对象时(不带初始化参数)
当定义 一个类对象 并且不提供 任何初始化参数时,默认构造函数 会自动被调用。如果 没有自定义构造函数,编译器 会生成 一个默认的空构造函数

class MyClass {
    
    
public:
    MyClass() {
    
     
        std::cout << "Default constructor called!" << std::endl;
    }
};

int main() {
    
    
    MyClass obj; // 默认构造函数被调用
    return 0;
}

2)类成员是对象时
如果 一个类 包含其他类类型的成员,并且 这些成员也有默认构造函数,当包含这些成员的类的构造函数 被调用时,这些成员的默认构造函数 也会自动调用

class A {
    
    
public:
    A() {
    
     
        std::cout << "Class A's default constructor!" << std::endl;
    }
};

class B {
    
    
    A a; // A 类的对象作为 B 类的成员
public:
    B() {
    
     
        std::cout << "Class B's default constructor!" << std::endl;
    }
};

int main() {
    
    
    B obj; // 先调用 A 的默认构造函数,然后调用 B 的默认构造函数
    return 0;
}

输出:
先调用 A 的默认构造函数,然后调用 B 的默认构造函数

Class A's default constructor!
Class B's default constructor!

当没有提供 默认构造函数
如果 Member 类中 没有默认构造函数,编译器 就无法自动调用 它的默认构造函数,必须显式地调用 其其他构造函数。否则,编译器会报错

#include <iostream>

class Member {
    
    
public:
    Member(int x) {
    
      // 只有一个参数化构造函数,没有默认构造函数
        std::cout << "Member constructor called with value: " << x << std::endl;
    }
};

class MyClass {
    
    
    Member m;  // Member 类的成员
public:
    MyClass(int x) : m(x) {
    
      // 必须使用初始化列表调用 Member 的构造函数
        std::cout << "MyClass constructor called" << std::endl;
    }
};

int main() {
    
    
    MyClass obj(42);
    return 0;
}

输出:

Member constructor called with value: 42
MyClass constructor called

没有使用初始化列表(不推荐)
如果 不使用初始化列表,成员变量 仍然会先通过 它们的默认构造函数初始化,然后在构造函数体中 再对它们进行赋值操作。这种方式效率较低,因为对象 会先被默认初始化,然后被赋予新的值

#include <iostream>

class Member {
    
    
public:
    Member(int x = 0) {
    
      // 提供默认参数
        std::cout << "Member constructor called with value: " << x << std::endl;
    }
};

class MyClass {
    
    
    Member m;  // Member 类的成员
public:
    MyClass(int x) {
    
    
        // 构造函数体内赋值,而不是使用初始化列表
        m = Member(x);
        std::cout << "MyClass constructor called" << std::endl;
    }
};

int main() {
    
    
    MyClass obj(42);
    return 0;
}

输出:

Member constructor called with value: 0  // 先调用默认构造函数
Member constructor called with value: 42 // 然后在构造函数体内赋值
MyClass constructor called

3)数组初始化时
当创建对象数组时,每个数组元素的默认构造函数 会被自动调用。如果数组中元素的类型是 自定义类,并且 没有提供初始值,那么每个元素 都会调用默认构造函数

class MyClass {
    
    
public:
    MyClass() {
    
    
        std::cout << "Default constructor called!" << std::endl;
    }
};

int main() {
    
    
    MyClass arr[3];  // 创建包含 3 个元素的数组,每个元素调用默认构造函数
    return 0;
}

输出:

Default constructor called!
Default constructor called!
Default constructor called!

4)动态分配对象时
当使用 new 操作符动态 分配一个类对象,并且 不提供构造函数的参数时,默认构造函数 会自动调用

class MyClass {
    
    
public:
    MyClass() {
    
     
        std::cout << "Default constructor called!" << std::endl;
    }
};

int main() {
    
    
    MyClass* ptr = new MyClass(); // 默认构造函数调用
    delete ptr; // 删除动态分配的对象
    return 0;
}

5)继承类时
当派生类的默认构造函数 被调用时,基类的默认构造函数 也会被自动调用。如果 派生类没有显式指定 基类构造函数,编译器会自动调用 基类的默认构造函数

class Base {
    
    
public:
    Base() {
    
     
        std::cout << "Base class default constructor called!" << std::endl;
    }
};

class Derived : public Base {
    
    
public:
    Derived() {
    
    
        std::cout << "Derived class default constructor called!" << std::endl;
    }
};

int main() {
    
    
    Derived obj; // 首先调用 Base 的默认构造函数,然后调用 Derived 的默认构造函数
    return 0;
}

总结:
创建对象时 不传递参数
动态分配对象时 不传递参数
类的成员是 对象类型时,其成员的默认构造函数 自动调用
创建对象数组时
继承关系中 派生类调用时,基类的默认构造函数 会被自动调用

4、参数是 data.begin(), data.end() 函数参数 应该怎么写

#include <iostream>
#include <vector>
#include <algorithm> // for std::for_each

// 定义一个模板函数,接受两个迭代器参数
template <typename InputIt>
void printRange(InputIt begin, InputIt end) {
    
    
    for (InputIt it = begin; it != end; ++it) {
    
    
        std::cout << *it << " ";
    }
    std::cout << std::endl;
}

int main() {
    
    
    std::vector<int> data = {
    
    1, 2, 3, 4, 5};

    // 调用函数,传递 data.begin() 和 data.end() 作为参数
    printRange(data.begin(), data.end());

    return 0;
}

让函数 可以对容器的任意范围进行操作,适应不同类型的容器,如 std::vector、std::list、std::set 等,且不需要 具体知道容器的类型,只要 容器支持迭代器就可以

std::for_each,可以直接 利用这些算法 而不需要手动遍历迭代器

std::vector<int> data = {
    
    1, 2, 3, 4, 5};

// 使用 std::for_each 打印范围内的元素
std::for_each(data.begin(), data.end(), [](int value) {
    
    
	std::cout << value << " ";
});

可以 通过指定迭代器类型,例如只接受 std::vector<int>::iterator 或者 std::list<int>::iterator,这样函数 只允许该特定类型的迭代器 作为参数

// 限制函数只接受 std::vector<int>::iterator 迭代器
void printVectorRange(std::vector<int>::iterator begin, std::vector<int>::iterator end) {
    
    
    while (begin != end) {
    
    
        std::cout << *begin << " ";
        ++begin;
    }
    std::cout << std::endl;
}

5、为什么报错

template <typename T, typename Container = std::vector<T>>
MaxHeap(Container::iterator begin, Container::iterator end)

由于 在模板类中 尝试直接访问 Container::iterator,但在模板参数中,Container 还不是一个具体的类型,因此 无法直接使用 Container::iterator
Container::iterator 需要通过 typename 来显式声明,因为在模板中,Container 是一个依赖类型,编译器 不能自动推导出 它包含的类型成员
改成

MaxHeap(typename Container::iterator begin, typename Container::iterator end) 

6、依赖类型,编译器 不能自动推导出它包含的类型成员:
依赖类型 是指 那些依赖于模板参数的类型。因为 这些类型在模板实例化之前 是未知的

C++编译器 在第一次看到模板时,并不会 对模板进行实例化,它只是 做基本的语法检查。因此,对于依赖于模板参数的名称,编译器 可能不知道 这个名称到底是类型 还是变量。为了明确表示这些依赖类型,C++标准引入了 typename 关键字
使用 typename 来明确告诉编译器:T::value_type 是一个类型

template <typename T>
class MyContainer {
    
    
public:
    typename T::value_type data;
};

template <typename T>
class MyContainer {
    
    
public:
    T::value_type data;  // 错误:没有 typename,编译器不知道 T::value_type 是什么
};

解析模板时的两个阶段:
实例化解析(模板实例化阶段):当模板 被具体类型实例化时,编译器 才会知道模板参数的具体类型,并解析 依赖于模板参数的类型

7、正在一个 const 成员函数中 返回一个非常量的引用。具体来说,top() 函数被声明为 const,但它试图返回 data[0] 的引用,而 const 成员函数 保证不会修改类的任何成员。因为 试图返回 data[0] 的非常量引用(就可以被修改),而 const 函数要求返回的值也是常量的(只有引用指针需要)

T& top() const {
    
    
    return data[0];  // data[0] 是一个非常量引用,但函数是 const
}

当一个成员函数被声明为 const 时,它意味着 该函数承诺 不会修改类的任何成员变量(除非 被声明为 mutable)
mutable 成员变量 可以在 const 成员函数中被修改,这是 唯一的例外

class MyClass {
    
    
private:
    int value;
    mutable int accessCount;  // 可以在 const 成员函数中修改

public:
    MyClass(int v) : value(v), accessCount(0) {
    
    }

    // 常量成员函数,不能修改 value,但可以修改 accessCount
    int getValue() const {
    
    
        accessCount++;  // 允许修改 mutable 变量
        return value;
    }

    int getAccessCount() const {
    
    
        return accessCount;
    }
};

int main() {
    
    
    MyClass obj(10);
    obj.getValue();  // 调用 getValue 后,accessCount 会被修改
    obj.getValue();
    std::cout << "Access count: " << obj.getAccessCount() << std::endl;  // 输出 2
}

  1. 返回值为非引用或非指针的情况
    如果 返回值是 值类型(如整数、浮点数 或 对象的副本),const 成员函数 并不要求 返回的值是常量的,因为返回的是 一个临时对象或拷贝,修改它 不会影响类的状态
class Example {
    
    
    int value;

public:
    Example(int v) : value(v) {
    
    }

    // const 成员函数:可以返回一个拷贝(值类型)
    int getValue() const {
    
    
        return value;  // 返回一个拷贝,不会影响对象状态
    }
};

  1. 返回引用或指针的情况
    如果函数返回的是 类成员的引用或指针,为了遵守 const 成员函数的约定(不修改对象状态),必须返回 常量引用 或 常量指针,这样调用者 就无法通过 返回值修改类的成员
class Example {
    
    
    int value;

public:
    Example(int v) : value(v) {
    
    }

    // const 成员函数:返回 const 引用
    const int& getValue() const {
    
    
        return value;  // 返回常量引用,调用者不能修改 value
    }

    // 非 const 成员函数:可以返回非常量引用
    int& getValue() {
    
    
        return value;  // 返回非常量引用,允许修改 value
    }
};

  1. 访问 const 对象的情况
    当有一个 const 对象时,只能调用 const 成员函数。这意味着 只有返回常量引用 或 常量指针的函数 才可以访问

4、高频面试题

1、如何从给定的无序数组中找到第 K 大的元素?

可以利用最小堆来解决这个问题。步骤如下:
创建一个大小为 K 的最小堆:
将数组的前 K 个元素添加到最小堆中
这可以使用 heapq.heapify() 函数在 O(k) 的时间内完成

遍历数组的剩余元素:
对于每个元素,如果其值 大于 最小堆的堆顶元素(即当前最小的元素),则:
用该元素替换堆顶元素
调整最小堆 以保持堆的性质,这个操作的时间复杂度是 O(log K)

获取结果:
遍历完成后,最小堆的堆顶元素即为第 K 大的元素

时间复杂度:O(n log k)

  1. 最小堆中 存储了 数组中最大的 K 个元素
    初始阶段:将数组的前 K 个元素放入最小堆,堆的大小为 K
    遍历阶段:对于数组中剩余的元素,如果 当前元素 大于 最小堆的堆顶(即最小的那个元素),就用 当前元素替换堆顶,然后重新调整堆(相当于把最小的那个元素删除了)
    通过这个过程,最小堆始终保留着 当前已知的最大的 K 个元素(重要)。因为:
    保持堆的大小为 K:任何时候堆中 都只存储 K 个元素
    堆的性质:在最小堆中,堆顶元素是堆中最小的

  2. 最小堆的堆顶是第 K 大的元素
    堆中元素的关系:最小堆中的所有元素 都是数组中最大的元素之一,但堆顶是 这些元素中最小的
    第 K 大的定义:第 K 大的元素 是在数组中从大到小排序后 位于第 K 位的元素
    因此,最小堆的堆顶元素是 当前最大的 K 个元素中最小的那个,也就是 整个数组中第 K 大的元素

2、插入和删除操作的 时间复杂度 O(log n)

3、堆排序是 一种利用堆结构的排序方法
将无序数组 构建成 一个最大堆
重复 从堆中删除最大元素,并将其 放到数组的尾部
时间复杂度:O(n log n)
空间复杂度:O(1)(就地排序)

4、二叉堆和斐波那契堆 有什么区别
二叉堆: 插入和删除操作的时间复杂度为 O(log n)。
斐波那契堆: 插入操作和减小关键字的时间复杂度为 O(1),删除最小元素的时间复杂度为 O(log n),但是均摊时间复杂度

减少键值和合并操作:斐波那契堆在这些操作上具有优势,尤其适用于需要频繁执行这些操作的算法

在这里插入图片描述

5、解释堆排序算法 以及 它的时间复杂度

堆排序算法的步骤如下:
构建最大堆
交换堆顶元素 与 最后一个元素,然后 减少堆的大小
重新堆化 新的堆顶元素
重复 上述步骤,直至堆的大小为 1
时间复杂度:O(n log n)

6、给定一个 k 有序数组,如何在最优时间复杂度内对它进行排序

k 有序数组是指 数组中的每个元素 最多只错位 k 个位置,也就是说,元素在排序后的位置 与原始位置的距离不超过 k。针对这种特殊的数组,可以在 O(n log k) 的时间复杂度内 对其进行排序,这比一般的排序算法(如 快速排序的 O(n log n))在 k 较小的情况下更高效

1)算法思路:
创建一个大小为 k+1 的最小堆(时间复杂度是 O(k)):
因为元素最多错位 k 个位置,所以前 k+1 个元素 包含了 数组开头需要的最小元素
将数组的前 k+1 个元素插入最小堆中

2)遍历数组并重建排序:
从数组的第 k+2 个元素开始,逐个遍历剩余的元素
对于每个元素:
从最小堆中取出堆顶元素(当前最小的元素,时间复杂度是 O(log k)),放到结果数组中
将当前元素插入最小堆(时间复杂度也是 O(log k))
当遍历完所有元素后,最小堆中还会剩下 k 个元素,将它们依次取出,放到结果数组中

3)总时间复杂度分析
初始化最小堆:O(k)
遍历和处理元素:
总共 n - (k+1) + k = n - 1 次 取出和插入操作
每次操作的时间复杂度为 O(log k)
总时间复杂度:O(k) + O((n - 1) * log k) ≈ O(n log k)

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

/**
 * 对 k 有序数组进行排序
 * @param arr 原始数组
 * @param k 元素错位的最大距离
 * @return 排序后的数组
 */
vector<int> sortKSortedArray(vector<int>& arr, int k) {
    
    
    int n = arr.size();
    vector<int> result;
    result.reserve(n);  // 预留空间,提升效率

    // 使用优先队列(最小堆)来管理当前的 k+1 个元素
    priority_queue<int, vector<int>, greater<int>> minHeap;

    // 将前 k+1 个元素插入最小堆
    for (int i = 0; i <= k && i < n; ++i) {
    
    
        minHeap.push(arr[i]);
    }

    // 从第 k+1 个元素开始遍历数组
    for (int i = k + 1; i < n; ++i) {
    
    
        // 取出堆顶元素(最小值),放入结果数组
        result.push_back(minHeap.top());
        minHeap.pop();
        // 将当前元素插入堆中
        minHeap.push(arr[i]);
    }

    // 将剩余的元素取出并加入结果数组
    while (!minHeap.empty()) {
    
    
        result.push_back(minHeap.top());
        minHeap.pop();
    }

    return result;
}

int main() {
    
    
    vector<int> arr = {
    
    2, 6, 3, 12, 56, 8};
    int k = 3;

    vector<int> sortedArr = sortKSortedArray(arr, k);

    cout << "排序后的数组: ";
    for (int num : sortedArr) {
    
    
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

https://kamacoder.com/ 手写简单版本STL,内容在此基础上整理补充

猜你喜欢

转载自blog.csdn.net/AsherGu/article/details/142992913