vlambda博客
学习文章列表

STL常用算法的使用与实现举例

容器、算法、迭代器是STL的几大部件。很多人学习STL都是从容器开始,导致后来也只熟悉容器,忽略了对算法的了解。其实算法也是STL非常强大和重要的一部分,容器和算法就像一个巨人强有力的左右手,它们彼此通过迭代器联系起来。

STL将很多常见的逻辑都封装为现成的算法,熟悉这些算法的使用和实现,很多时候可以大大简化编程,并且在需要的时候能够对STL进行扩展,将自定义的容器和算法融入到STL中。

日常的算法逻辑可能都是与数组下标打交道,STL的算法是基于迭代器,在操作习惯和姿势上会有少许的不同,在对于迭代器常用的next、distance、advance、iter_swap等操作都习惯以后,才能更加熟练彻底地掌握STL,要熟悉这些操作,需要自己做一些练习。这里列举几个常见的算法,都是作为练习不错的素材,自己尝试实现一下,可以感觉对迭代器的熟悉程度有明显的提高。

std::adjacent_find

在一个迭代器区间中查找两个相邻的相等的元素。也可以通过传入一个回调函数,查找满足回调函数的两个连续的元素。

【用法示例】 leetcode第941题:给定一个整数数组 arr,如果它是有效的山脉数组就返回 true,否则返回 false。

让我们回顾一下,如果 A 满足下述条件,那么它是一个山脉数组:arr.length >= 3 在 0 < i < arr.length - 1 条件下,存在 i 使得:arr[0] < arr[1] < ... arr[i-1] < arr[i], arr[i] > arr[i+1] > ... > arr[arr.length - 1]

这题用std::adjacent_find可以这样简单优雅地实现:

bool validMountainArray(vector<int>& arr) {
    return 1 == distance(
        adjacent_find(arr.cbegin(), arr.cend(), greater_equal<int>()),
        adjacent_find(arr.crbegin(), arr.crend(), greater_equal<int>()).base());
}

要写出这个代码,需要对reverse_iterator有一定的了解。

adjacent_find算法本身的实现比较简单,可以这样实现:

template<class ForwardIt>
ForwardIt adjacent_find(ForwardIt first, ForwardIt last)
{
    if (first == last) {
        return last;
    }
    // 用first和next表示相邻的两个元素
    ForwardIt next = first;
    ++next;
    for (; next != last; ++next, ++first) {
        if (*first == *next) {
            return first;
        }
    }
    return last;
}

std::unique

删除一个区间内连续相同的重复元素,连续相同的元素只保留一个。

【用法示例】 leetcode第26题:给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

这题可以直接用std::unique:

int removeDuplicates(vector<int>& nums) {
    auto it = std::unique(nums.begin(), nums.end());
    return std::distance(nums.begin(), it);
}

unique的一种实现:

template<class ForwardIt>
ForwardIt unique(ForwardIt first, ForwardIt last)
{
    if (first == last)
        return last;
 
    ForwardIt result = first;
    while (++first != last) {
        if (!(*result == *first) && ++result != first) {
            *result = std::move(*first);
        }
    }
    return ++result;
}

对于std::unique的实现方法我前几年在招聘的时候曾经不止一次的将其作为笔试题的第一题,有非常好的甄选效果。实现方法也是常用的双指针,一个快一个慢,在代码的写法上也不止一种,每个人的写法不同,对于别人的代码可以单步调试加速理解。

std::partition

将一个区间按照一定的条件(用回调函数判断)分成两个部分。

【用法示例】 leetcode第905题:按奇偶排序数组,给定一个非负整数数组 A,返回一个数组,在该数组中, A的所有偶数元素之后跟着所有奇数元素。你可以返回满足此条件的任何数组作为答案。

这题可以用std::partition来解决

vector<intsortArrayByParity(vector<int>& A) {
    partition (A.begin (), A.end (), [](auto i){return !(i&1);});
    return A;
}

std::partition的一种实现方法:

template<class ForwardIt, class UnaryPredicate>
ForwardIt partition(ForwardIt first, ForwardIt last, UnaryPredicate p)
{
    first = std::find_if_not(first, last, p);
    if (first == last) return first;
 
    for (ForwardIt i = std::next(first); i != last; ++i) {
        if (p(*i)) {
            std::iter_swap(i, first);
            ++first;
        }
    }
    return first;
}

std::partition可以直接用来实现quicksort,因为对于迭代器的要求是双向迭代器,因此实现的quicksort可以对std::list使用。

std::rotate

旋转一个区间。

【用法示例】 leetcode第189题:旋转数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

直接用std::rotate可以解题:

void rotate(vector<int>& nums, int k) {
    int distance = nums.size() - k % nums.size();
    std::rotate(nums.begin(), nums.begin() + distance, nums.end());
}

std::rotate的实现方法有好几种,我一直记得最清楚的是这种:对整体做一次reverse操作,然后对前k个元素和剩下的N-k个元素分别做一个reverse操作:

void rotate(vector<int>& nums, int k) {
    k %= nums.size();
    reverse(begin(nums), end(nums));
    reverse(begin(nums), begin(nums)+k);
    reverse(begin(nums)+k, end(nums));
}

std::next_permutation

给出某个区间按照字典顺序排列的下一个序列,如果没有下一个序列,就返回false。

【用法示例】 leetcode第46题:全排列,给定一个 没有重复 数字的序列,返回其所有可能的全排列。

用std::next_permutation可以直接解这一题:

vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> result;
    std::sort(nums.begin(), nums.end());
    do {
        result.push_back(nums);
    }while(std::next_permutation(nums.begin(), nums.end()));
    return result;
}

std::next_permutation的实现分成三步:从右到左找到第一个非逆序的元素,假设是第k个,然后将该元素与右边大于该元素的且差距最小的元素进行交换,然后再将从第k+1个开始的元素全部reverse。

以1 3 7 10 8 5 4 1这个序列举例:1、从右向左找到第一个非逆序的元素,找到7;2、7的后边比7大且差距最小的数字是8,与8交换,得到:1 3 8 10 7 5 4 1 3、将8后面的序列逆序,得到:1 3 8 1 4 5 8 10,即为要求的下一个序列。

next_permutation的一种实现方法:

template<class BidirIt>
bool next_permutation2(BidirIt first, BidirIt last)
{
    if (first == last) return false;
    BidirIt i = last;
    if (first == --i) return false;
 
    while (true) { 
        BidirIt i1 = i;
        if (*--i < *i1) {
            BidirIt i2 = last;
            while (!(*i < *--i2))
                ;
            std::iter_swap(i, i2);
            std::reverse(i1, last);
            return true;
        }
        if (i == first) {
            std::reverse(first, last);
            return false;
        }
    }
}


总结

STL的算法还有很多,可以多选一些出来练手,在实现这些算法的时候,尤其对于边界条件要多注意测试,才能写出严谨的代码。另外,可以配合C++20的concept来写,用concept实现出来的代码不会出现几千行的编译错误。