容器、迭代器和算法


这是《深入实践C++模板编程》第五章“容器、迭代器和算法”的读书笔记。

容器、迭代器和算法

通过C++模板可以将类型以及其他编译期常数作为参数抽离出来,使代码拜托对类型依赖,从而设计容纳不同类型的容器成为可能。

  • 容器是指专门用于存储某种形式组织及存储的类。

容器的实现

常见的容器有数组、链表、集合、关联组等。通常语言本身会提供几种基本容器,通过这些基本容器可以组织更复杂的容器。这些容器的共同点是可以容纳多种类型的数据

弱类型的动态语言,对于数据类型不敏感,其提供的类型天然支持多种数据类型。例如Python中的list,dict,set等,可以在同一个容器实例中保存不同类型的数据,实现异质容器。

强类型语言,变量以及函数惨了类型固定变,这是设计支持多种数据类型容器的最大障碍。C++通过模板,使得容器和算法结构不再依赖具体类型;但是这并没有实现真正支持多种类型的容器类。支持不同类型,只是生成了不同类型的容器类。

除了C++模板,其他强语言类型也有各自容器实现方法。这里介绍Java实现容器方法。

Java的实现方法

与C++相比,Java是更高级的面相对象语言。Java中所有类都必须同一颗继承树上的某个节点;这颗继承树的根节点是类Object。除了几种有限的“原始(primitive)”类型外,所有类型都是标准根节点Object的派生类。即使是“原始”类,在继承树上也有对应的封装类。

Java的任何实例,可以**上转义(upcast)**成为基类实例,所以所有类都可以转义为Object实例。且Java中的变量是一个类实例的引用,从C++角度看,变量都是指针;所以变量值传递只是地址的传递,不会有任何数据损失。

正因为上面这两个特点,Java中的单一容器可以容纳多种数据类型。容器只需设计为保存Object类型数据,任何实例都可以上转义为Object类型实例后放入容器,从容器取数据时,必须**下转义(downcast)**为所需类型实例引用。

Java这种实现存在风险,例如将类型A实例上转义为Object类型保存到容器,取出时可以下转义为B类型。在语法上没有错误,编译时只会给出警告,如果有错误,只有在运行时Java解释器才会发现这个非法转义操作。

从Java SE(Java Standard Environment)1.5版本开始,引入了**泛型(Generic)**概念,可以约束容器只接纳一种数据类型,这也是新版Java推荐的做法。

Java中的泛型和C++中模板看似类似,但是本质不同。模板是根据类型在编译期生成不同类型容器,泛型只是增加约束,在编译期做了额外检查以及取出时自动做了类型转换,Java中容器实现并没有改变。

C++的实现方法

C++没有Java的那两个特点

  • 没有官方继承树。
  • 变量不是引用,即变量不是地址。

定义继承树,就会限制C++开发者的自由,且C++必须显式集成。变量不是引用,做变量类型转换上转义时,会有数据丢失,即对象切割(object slicing)

C++中是否可以在容器中保存地址,即指针?可以,但是比较危险。C++的内存需开发者自己管理,保存在容器中,何时释放、由谁释放都是问题,稍有不慎,就会出错。

C++中用模板实现容器,是其语言本身特性决定的。在当下,使用模板实现容器是明智的选择;但也不排除后续C++引入新的特性,就会有新的容器实现方式。

容器与迭代器

容器可以存储数据,从容器中取数据,也需要统一的标准,即迭代器(iterator)

迭代器设计类似于指针思想。通过遍历数组,可以得到启发。例如有一个数组array[],遍历它可以通过两种方式:

// 遍历法一
for (unsigned i = 0; i < sz; ++i)
    do_something(array[i]);

// 遍历法二
int* begin = array;
int* end = begin + sz;
for(int* it = begin; it != end; ++it)
    do_somethin(*it);

第一种遍历方法比较直观,它依赖于数组内存连续。迭代器设计类似于第二种遍历方法,通过++it来找到下一个元素位置,可以通过容器内在元素之间的联系实现++it

容器中的元素,总可以通过某种规则排列成一个虚拟序列。例如链表天然就是一个序列;二叉树可以按照前序、中序、后序遍历成一个序列;图可以按照广度优先或深度优先排序成一个序列。

容器可以通过封装代理类实现迭代器。例如重载自加操作符++指向序列中下一个元素,重载*取数据。

下面实现两个简单的容器及其迭代器。

链表容器与迭代器

/ list_iterator.hpp
#pragma once
#include <stdexcept>

template<typename T> class list; // forward declare

template<typename N>
class list_iterator {
    N* pos;

    template<typename T> friend class list;
public:
    typedef typename N::value_type value_type;
    typedef typename N::reference_type reference_type;
    typedef typename N::const_reference_type const_reference_type;
    typedef list_iterator<N> self_type;

    list_iterator() : pos(0) {}
    list_iterator(N* pos) : pos(pos) {}

    bool operator != (self_type const& right) const {
        return pos != right.pos;
    }

    bool operator == (self_type const& right) const {
        return pos != right.pos;
    }

    self_type& operator ++ () {
        if (pos) pos = pos->next;
        return *this;
    }

    reference_type operator * () throw (std::runtime_error) {
        if (pos) return pos->value;
        else throw (std::runtime_error("deferenct null iterator!"));
    }
};


// list.hpp
#pragma once
#include <stdexcept>
#include "list_iterator.hpp"

template<typename T>
struct list_node {
    typedef T value_type;
    typedef T& reference_type;
    typedef const T& const_reference_type;

    T value;
    list_node* prev;
    list_node* next;

    list_node(T const& value, list_node* prev, list_node* next) :
        value(value), prev(prev), next(next) {}
};

template<typename T>
class list {
    typedef list_node<T> node_type;
    node_type *head;

public:
    typedef T value_type;
    typedef list_iterator<node_type> iterator;
    list(): head(nullptr) {}
    ~list() {
        while (head) {
            node_type* n = head;
            head = head->next;
            delete n;
        }
    }

    void push_front(const T& v) {
        head = new node_type(v, 0, head);
        if (head->next) head->next->prev = head;
    }

    void pop_front() {
        if (head) {
            node_type* n = head;
            head = head->next;
            head->prev = nullptr;
            delete n;
        }
    }

    void insert(iterator it, const T& v) {
        node_type* n = it.pos;
        if (n) {
            node_type* new_node = new node_type(v, n, n->next);
            new_node->next->prev = new_node;
            n->next = new_node;
        }
    }

    void erase(iterator& it) {
        node_type *n = it.pos;
        ++it;
        if (n) {
            if (n->next) n->next->prev = n->prev;
            if (n->prev) n->prev->next = n->next;
            if (head == n) head = head->next;
            delete n;
        }
    }

    bool is_empty() const {return head == 0;}

    iterator begin() {return iterator(head);}
    iterator end() {return iterator();};
};

// test.cpp
#include <iostream>
#include "list.hpp"

int main(int argc, char* argv[]) {
    list<int> l;
    for (int i = 0; i < 10; ++i) l.push_front(i);

    for(auto it = l.begin(); it != l.end(); ++it) {
        std::cout << *it << std::endl;
    }

    return 0;
}


集合与迭代器

集合通常采用红黑树实现,这个为了简单,采用二叉搜索树实现。假设树节点有公共成员变量value,公共成员变量parentleftright是分别指向其父节点、左右节点的指针。二叉树中序遍历得到的序列是从小到大,所以遍历时采用中序。

// tree_iteartor.h
#pragma once
#include <stdexcept>

template<typename N>
class tree_iterator {
    const N* pos; // 当前位置
public:
    typedef typename N::value_type value_type;
    typedef typename N::const_referenct_type const_referenct_type;
    typedef tree_iterator<N> self_type;

    tree_iterator() : pos(nullptr) {}
    tree_iterator(const N* pos) : pos(pos) {}

    bool operator == (self_type const& right) const {
        return pos == right.pos;
    }

    self_type& operator++ () {
        if (pos) {
            if (pos->right) {
                pos = pos->right;
                while (pos->left) pos = pos->left;
            }
            else {
                while ((pos->parent) && (pos->parent->right == pos)) {
                    pos = pos->parent;
                }
                pos = pos->parent;
            }
        }
        return *this;
    }

    const_referenct_type operator* () const throw (std::runtime_error) {
        if (pos) return pos->value;
        else throw std::runtime_error("dereferencing null iterator!");
    }
};

template<typename N>
bool operator != (tree_iterator<N> const& left, tree_iterator<N> const& right) {
    return !(left == right);
}


// set.h
#pragma once
#include "tree_iterator.h"

#include <iostream>

template<typename T>
struct tree_node {
    typedef T value_type;
    typedef T& reference_type;
    typedef const T& const_referenct_type;

    T value;
    tree_node* parent;
    tree_node* left;
    tree_node* right;

    tree_node(T const& value, tree_node* parent, tree_node* left, tree_node* right) :
        value(value), parent(parent), left(left), right(right) {}
    ~tree_node() {
        if (left) delete left;
        if (right) delete right;

    }
};

template<typename T>
class set {
    typedef tree_node<T> node_type;
    node_type* root;
public:
    typedef T value_type;
    typedef tree_iterator<node_type> const_iterator;

    set(): root(nullptr) {}
    ~set() { if(root) delete root;}

    bool insert(const T& v) {
        node_type** n = &root;
        node_type* p = nullptr;
        while (*n) {
            if (v == (*n)->value)  // 集合中已经有该值
                return false;
            else {
                p = *n;
                n = v < (*n)->value ? &((*n)->left) : &((*n)->right);
            }
        }
        *n = new node_type(v, p, nullptr, nullptr);
        // std::cout<< "insert ["<< v << "] , parent [" << (p ? p->value: 0 )<< "]" << std::endl;
        return true;
    }

    bool has(const T& v) {
        node_type *n = root;
        while (n) {
            if (v == n->value)
                return true;
            n = v < n->value ? n->left : n->right;
        }
        return false;
    }

    bool is_empty() const {return root == nullptr;}

    const_iterator begin() const {
        // 定位到最左节点,即中序遍历的第一个节点
        node_type *n = root;
        while (n->left)
            n = n->left;
        return const_iterator(n);
    }

    const_iterator end() const {return const_iterator();}
};

// test_set.cpp
#include "set.h"

#include <stdlib.h>
#include <iostream>

int main(int argc, char* argv[]) {
    set<int> my_set;
    for (int i = 0; i < 10; ++i) {
        int num = rand() % 20;
        std::cout << num << "--";
        my_set.insert(num);
    }

    std::cout << std::endl;
    int count = 0;
    for(auto it = my_set.begin(); it != my_set.end(); ++it) {
        std::cout << *it << "--";
        ++count;
        if (count > 10) break;
    }
    std::cout << std::endl;
}

上面两个容器不同,但是迭代器有着相同的接口:可以通过++找到下一个元素,可以通过*解引用。

迭代器与算法

有了迭代器之后,遍历容器或者访问容器某个区间可以和具体容器解耦和,使得通用性算法实现变得可能。

求容器中元素纸盒

最直观的实现是在函数内调用begin()得到其实元素,之后累加

template<typename C>
typename C::value_type
sum(C& c) {
	typedef typename C::value_type value_type;
	typedef typename C::iterator iterator;
	value_type sum(0);
	for (iterator i = c.begin(); i != c.end(); ++i) sum += *i;
	return sum;
}

上面的函数可以计算大部分容器,但是还有2点不足:1、无法求解部分元素之和;2、不兼容数组(C++11已经兼容了),因为数组内没有嵌套定义value_type。下面实现一个求解区间元素之和的函数

template<typename I>
typename I::value_type
sum(I begin, I end) {
	typedef typename I::value_type value_type;
	
	value_type sum(0);
	for(; begin != end; ++begin) sum += *begin;
	return sum;
}

上面代码已经可以求解区间元素的和了。但是还是不支持数组;数组内没有嵌套定义value_type,无法知道数组内元素类型。

要统一数组和容器,还需要借助另外一个类模板及其模板特例来统一描述迭代器和指针。这个模板只是用来嵌套定义元素值类型,如果模板参数是迭代器,则重新定义值类型,如果是指针,则在模板中为指针定义元素类型。

template<typename I>
struct iterator_traits {
    typedef typename::I::value_type value_type;
};

template<typename P>
struct iterator_traits<P*> {
    typedef P value_type;
};

template<typename I>
typename iterator_traits<I>::value_type
sum(I begin, I end) {
    typedef typename iterator_traits<I>::value_type value_type;
    
    value_type sum(0);
    for (; begin != end; ++begin) sum += *begin;
    return sum;
}

微型算法库

这个库由一个头文件组成,可以对容器元素求和,还可以定位元素、打印元素移位等。

#include <iostream>

template<typename I>
struct iterator_traits {
    typedef typename::I::value_type value_type;
};

template<typename P>
struct iterator_traits<P*> {
    typedef P value_type;
};

// 打印
template<typename I>
void print(I begin, I end) {
    if (begin != end) {
        std::cout << *begin;
        for (++begin; begin != end; ++begin)
            std::cout << " " << *begin;
        std::cout << std::endl;
    }
}

// 求和
template<typename I>
typename iterator_traits<I>::value_type
sum(I begin, I end) {
    typedef typename iterator_traits<I>::value_type value_type;
    
    value_type sum(0);
    for (; begin != end; ++begin) sum += *begin;
    return sum;
}

// 指定范围内数据循环移位
template<typename I>
void shift(I begin, I end) {
    typedef typename iteartor_traits<T>::value_type value_type;
    
    I it = begin;
    if (it != end) {
        value_type v = *begin;
        value_type tmp;
        
        for (++it; it != end; ++it) {
            tmp = *it;
            *it = v;
            v = tmp;
        }
        *begin = v;
    }
    return;
}

// 容器内查找并定位指定数据
template<typename I>
I find(I begin, I end, typename I::value_type const& v) {
    for(; begin != end; ++begin)
        if (*begin == v) break;
    return begin;
}

容器和迭代器的分类

上面实现的链表和集合代表了两类容器:序列型容器(sequence container)和关系型容器(associative container)

  • 序列型容器:数据存储位置和内容无关,通常按照一定的序列组织,例如数组、链表、向量等。
  • 关系型容器:数据存储位置和内容有关,例如集合、字典、散列表等。

序列型容器提供的迭代器可以修改数据,而关系型容器则不可以(是指不可以修改key)。

按照迭代器在容器上的移动能力,可以分为三类:

  • 前向迭代器(forward iterator):只能移动到下一个元素,例如单向链表的迭代器。这也是最基本的迭代器。
  • 双向迭代器(bidirectional iterator):可以移动到下一个或上一个元素的迭代器。
  • 随机迭代器(random iterator):可以前向或后向跳若干个元素的迭代器。

容器的陷阱

迭代器引入了一致性的陷阱,写代码时需要小心。有以下几点需要注意:

  • 容器中,以迭代器为参数对容器进行操作,该迭代器必须来自此容器,否则会有undefined behavior。
  • 一次性使用多个迭代器,要防止彼此之间冲突。
  • 要防止迭代器失效。如果使用迭代器在容器中删除了元素,那么operator()++很可能失效,因为删除操作可能改变了容器结构。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值