北大C++程序设计编程作业答案+解析·标准模板库STL(一)

本章一共包含七个编程习题:

  1. goodcopy
  2. 按距离排序
  3. 很难蒙混过关的CArray3d三维数组模板类
  4. 函数对象的过滤器
  5. 白给的list排序
  6. 我自己的 ostream_iterator
  7. List

以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))

1. goodcopy

考点:函数模板

解析:这里题目要求把来源数组中的一定范围的元素,拷贝到目标数组中,最简单的方法就是遍历来源数组的范围(使用iterator),然后一个个拷贝给目标数组,这个思路是没有问题的,但是注意题目里面会把同一个数组的一部分,拷贝到这个数组的另一个范围里面:

GoodCopy<int>()( a, a + m, a + m / 2 );

这里表示把元素范围[ a, a + m - 1 ]拷贝到[ a + m / 2, a + m / 2 + m - 1 ],如果还用上面的思路,那么就会出现问题,我们看看下面这个例子:

// char数组A:
0    1    2    3    4    5   6
a    b    c    d    e
GoodCopy<int>()( A, A + 2, A + 2 / 2 );
// 表示把元素范围[ A, A + 2 - 1 ]拷贝到[ A + 2 / 2, A + 2 / 2 + 2 - 1 ],即[ A, A + 1 ] => [ A + 1, A + 2 ]
// 用下标来表示就是:[ 0, 1 ] => [ 1, 2 ]
// 我们发现如果从左到右进行拷贝,下标为1的元素会在第一次复制时,被覆盖:
0    1    2    3    4    5   6
a    a    c    d    e
^    ^
|    |
  =>

所以我们不能使用从左到右的方法进行拷贝,那么该怎么办呢?这里我们想想,如果我们反过来进行拷贝呢,也就是先右再左,这样就不会有覆盖的问题了,这是因为如果从左向右,来源范围和目标范围有一定的重叠区域会被覆盖,只有反过来进行复制才不会这个问题:

0    1    2    3    4    5   6
a    b    c    d    e
     a    b    c    d    e
     ^
     |
  重叠区域

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
// 在此处补充你的代码
template<class T>
struct GoodCopy {
public:
    void operator()( T *s, T *e, T *D ) {
        // 有多少个元素需要拷贝
        int n = e - s; // How many elements we wanna copy
        assert( n >= 0 );

        // 0个元素需要拷贝,不做任何操作
        if ( !n ) return; // Zero element to be copied.

        // 调整指针指向右边进行复制
        D += n - 1;
        e--;
        // 从右向左进行遍历复制
        for ( ; s != e; e-- ) {
            *D = *e;
            D--;
        }

        // 不要忘记复制最左边的元素
        *D = *e; // equivalent to *D = *s
    }
};

2. 按距离排序

考点:类模板

解析:此处题目要求依据某个基底元素进行比较排序,而且排序的两个数组是不同类型,所以我们需要使用模板,并且包含两个模板类型,一个是排序数组的元素类型,一个是排序比较需要用到的函数指针类型:

template<class T1, class T2>
struct Closer { // 其他略 }

然后题目是将初始化的对象当作比较函数传给std::sort()的,所以需要重载其运算符(),使其能够当函数调用:

bool operator()( const T1 &a_, const T1 &b_ ) {}

函数内部只需调用比较函数比较数组两个元素与基底元素的大小关系即可。

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
// https://en.cppreference.com/w/cpp/algorithm/sort
template<class T1, class T2>
struct Closer {
    // 在此处补充你的代码
    T1 &b; // base
    T2 f; // functor

public:
    Closer( T1 &b_, T2 f_ ) : b( b_ ), f( f_ ) {}

    bool operator()( const T1 &a_, const T1 &b_ ) {
        // Compare a_ and b_ with the base
        int r = f( a_, b ) - f( b_, b );
        return r == 0 ? // 两元素距离基底元素相当么?
            a_ < b_ : // 相等,则按字典顺序比较
        	r < 0; // 不相等,则小的在前面
    }
};

3. 很难蒙混过关的CArray3d三维数组模板类

考点:内部类,类模板,运算符重载

解析:这里需要实现一个3D Array,我们先来分析需要重载哪些运算符:

a[ i ][ j ][ k ] = No++;
// 根据题目的提示1,这里有三层运算符运算,但又两层需要重载:
// 第一层:a[ i ] => 重载CArray3D的运算符[],返回一个二维数组类,这里定义为Array2D
// 第二层:a[ i ][ j ] => 相当于Array2D[ j ],那么就需要重载Array2D的运算符[],返回一个一维数组指针:T *
// 第三层:a[ i ][ j ][ k ] => 相当于T *[ k ],题目这里给的类型是int,所以不需要重载,直接取值

对于Array2D,不知道大家还记不记得之前我们做过一道相似的题目:二维数组类(运算符重载章节),这里我们就可以复用之前的代码了,所以第二层的运算符重载我们用之前的就行,如果有不清楚的童鞋,可以参考之前的文章。那么这里我们只需重载CArray3D的运算符[]:

Array2D &operator[]( size_t i ) {
    return *A[ i ];
}

注意这里需要返回Array2D的引用,因为题目中我们需要进行值修改的操作,返回对象是不会更改原来数组中的元素的,这里大家需要注意哦~

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
template<class T>
class CArray3D {
    // 在此处补充你的代码
    // https://github.com/fengkeyleaf/peking_cpp/blob/master/hw4/include/twoD_array.h
    class Array2D {
        size_t r;
        size_t c;
        T *A;

    public:
        Array2D() {
            r = c = 0;
            A = nullptr;
        }

        Array2D( size_t r, size_t c ) {
            this->r = r;
            this->c = c;
            A = new T[ r * c ];
        }

        ~Array2D() {
            if ( A ) delete [] A;
        }

        T *operator[]( size_t r ) {
            return r * c + A;
        }

        T &operator()( size_t r, size_t c ) {
            return A[ r * this->c + c ];
        }

        Array2D &operator=( const Array2D &A ) {
            if ( this->A ) delete [] this->A;

            c = A.c;
            r = A.r;
            size_t s = A.c * A.r;
            this->A = new int[ s ];
            std::copy( A.A, A.A + s, this->A );
            return *this;
        }

        operator T*() {
            return A;
        }
    };

    Array2D **A;
    size_t i;

public:
    CArray3D( size_t i_, size_t j_, size_t k_ ) {
        i = i_;
        A = new Array2D*[ i ];
        for ( size_t j = 0; j < i_; j++ )
            A[ j ] = new Array2D( j_, k_ );
    }

    ~CArray3D() {
        for ( size_t i_ = 0; i_ < i; i_++ )
            delete A[ i_ ];

        delete [] A;
    }

    Array2D &operator[]( size_t i ) {
        return *A[ i ];
    }
};

4. 函数对象的过滤器

考点:类模板,运算符重载

解析:题目要求实现一个函数过滤功能,过滤器的作用是过滤不在范围内的元素,所以类里面我们需要存储这个过滤范围,并且使用类模板,因为我们需要过滤不同类型的数组,最后需要重载运算符(),这是因为题目直接把过滤器对象当作函数调用:

std::vector<int>::iterator p = Filter( 
    ia.begin(), 
    ia.end(), 
    ib.begin(),
    // 对象当作函数调用,需要重载运算符()
    FilterClass<int>( m, n )
);

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
// 在此处补充你的代码
template<class T>
struct FilterClass {
    T m;
    T n;

public:
    FilterClass( T m_, T n_ ) : n( n_ ), m( m_ ) {}

    bool operator()( const T &t ) {
        return m < t && t < n;
    }
};

5. 白给的list排序

考点:std::sort()函数的使用

解析:这题超级简单,需要调用std::sort()进行降序排序,STL里面有内置的降序排序算法std::greater<Type>(),直接调用即可,另外,std::less<Type>()就是升序算法,这里两个算法都是定义:lh > rh 或 lh < rh,即以左边的元素作为基准进行比较,大家需要注意一下。

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
// 在此处补充你的代码
// https://en.cppreference.com/w/cpp/container/list/sort
// https://en.cppreference.com/w/cpp/utility/functional/greater
lst.sort( std::greater<double>() );

6. 我自己的 ostream_iterator

考点:类模板,std::ostream_iterator

解析:题目要求实现我们自己的ostream_iterator,所以先来分析一下需要实现哪些功能:

const int SIZE = 5;
int a[SIZE] = { 5, 21, 14, 2, 3 };
double b[SIZE] = { 1.4, 5.56, 3.2, 98.3, 3.3 };
std::list<int> lst( a, a + SIZE );
// 需要实现构造函数
myostream_iteraotr<int> output( std::cout, "," );
Copy( lst.begin(), lst.end(), output );
std::cout << std::endl;
myostream_iteraotr<double> output2( std::cout, "--" );
Copy( b, b + SIZE, output2 );

template<class T1, class T2>
void Copy( T1 s, T1 e, T2 x ) {
    // 需要重载运算符++
    for ( ; s != e; ++s, ++x )
        *x = *s; // 需要重载运算符*和=,进行取值和赋值操作
}

接下来就是怎么实现了,既然STL里面已经有相似的实现,为什么不去参照一下大佬们是怎么写的呢?

// std::ostream_iterator的构造函数
template<typename _Tp, typename _CharT = char,
       typename _Traits = char_traits<_CharT> >
class ostream_iterator
: public iterator<output_iterator_tag, void, void, void, void>
{ 
    // 其他略 
}

这里有三个模板参数:_Tp,_CharT和_Traits,第一个表示需要写进ostream的元素类型,第二个表示ostream存储的元素类型,第三个这里用不到,我们暂时跳过,因为题目是传入std::cin,所以这里前两个类型都是char,因为std::cin就是std::basic_ostream<char>的别名,那么我们的构造函数需要有两个参数,一个是std::cin,另一个是输出分隔符,可以使用const char*,也可以是std::string,但是这里我们用前一个,因为STL里面用的也是相似的结构:

ostream_iterator(ostream_type& __s, const _CharT* __c)
: _M_stream(std::__addressof(__s)), _M_string(__c)  {}

接下来是运算符++的重载:

ostream_iterator&
operator++()
{ return *this; }

ostream_iterator&
operator++(int)
{ return *this; }

这里STL只是返回了原对象的引用,接下来是运算符*和=的重载:

ostream_iterator&
operator*()
{ return *this; }

ostream_iterator&
operator=(const _Tp& __value)
{
    __glibcxx_requires_cond(_M_stream != 0,
            _M_message(__gnu_debug::__msg_output_ostream)
            ._M_iterator(*this));
    *_M_stream << __value;
    if (_M_string)
      *_M_stream << _M_string;
    return *this;
}

我们可以发现STL重载运算符*还是返回源对象的引用,但是运算符=重载中,先是将需要输出的元素载入到stream中,之后判断是否有分隔符,有则输出分隔符,并且还是返回源对象的引用,也就是说*x返回stream对象,=*s对元素s进行流式输出,并且输出分隔符,因此,我们可以照葫芦画瓢,编写我们自己的ostream_iterator。

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
// 在此处补充你的代码
template<class T>
class myostream_iteraotr {
    std::basic_ostream<char>& is;
    const char* d = nullptr;

public:
    myostream_iteraotr( std::basic_ostream<char>& is_, const char* delimit_ ) : is( is_ ), d( delimit_ ) {}

    myostream_iteraotr& operator++() {
        return *this;
    }

    myostream_iteraotr& operator=( const T& v ) {
        is << v;
        if ( d ) is << d;

        return *this;
    }

    myostream_iteraotr& operator*() {
        return *this;
    }
};

7. List

考点:类模板,std::map

解析:首先我们来分析一下题目需要实现的List功能:

new id ——新建一个指定编号为id的序列(id < 10000)

=> 这个Llist的元素是一个列队,那么我们也可以用Vector,也就是二维数组,id可以映射到下标

add id num——向编号为id的序列加入整数

=> 这个就是简单的vector的push_back()操作

num merge id1 id2——如果id1等于id2,不做任何事,否则归并序列id1和id2中的数,并将id2清空

=> 把序列id2的元素加入到序列id1中,但是需要有序,这里我们可以用到std::multiset,有序container,并且允许重复元素

unique id——去掉序列id中重复的元素

=> 去掉序列中的重复元素,可以用到std::set,有序但不允许重复元素

out id ——从小到大输出编号为id的序列中的元素,以空格隔开

=> 按升序进行输出序列中的元素,那么我们需要排序之后再输出

上述的分析思路理论上是没有问题的,但是实际中,id不一定连续,如果用二维数组存储,会有许多空位置,所以这里我们外层使用std::map进行包装,id为Key,vector为Value,所以上述每个操作的时间和空间复杂度如下:

OpTime complexitySpace complexity
new idO( logn )O( n )
add id numO( logn )O( n )
num merge id1 id2O( 2 * h ) + O( klog( k ) ) + O( k ), where h = min( m, n ), k = m + n, m = len( id1), n = len( id2 )O( k + h + k )
unique idO( nlog( n ) )O( n )
out idO( nlog( n ) ) + O( n )O( n )

注意在merge操作里面,我们需要下面几步来完成相关操作:

template<class T>
void MyVector<T>::merge( size_t id1, size_t id2 ) {
    if ( id1 == id2 ) return;

    if ( M.find( id1 ) == M.end() || M.find( id2 ) == M.end() ) {
        std::cerr << "No such a list named " << id1 << " or " << id2 << std::endl;
        return;
    }

    // 将序列id2的元素加入到序列id1之中,这里可以进行优化,只加入长度最小的数组 => O( h )
    for ( T& t : M[ id2 ] ) M[ id1 ].push_back( t );
    // 清空序列id2的元素,同样可以优化成只清空长度最小的序列 => O( h )
    M[ id2 ].clear();

    // 将序列id1中的元素全部插入std::multiset中,没插入一个元素是O( logk ),k个元素是O( klog( k ) ) => O( klog( k ) )
    std::multiset<T> S( M[ id1 ].begin(), M[ id1 ].end() );
    // 将所有元素拷贝给序列id1 => O( k )
    M[ id1 ].assign( S.begin(), S.end() ); 
}

答案:完整源码地址

// 这里只给到需要相关定义,完整代码请移步到github
// 在此处补充你的代码
template<class T>
class MyVector {
    // https://en.cppreference.com/w/cpp/container/map
    std::map<size_t, std::vector<T>> M;

public:
    void append( size_t id, T& t_ );

    void add_new_list( size_t id_ );

    void merge( size_t id1, size_t id2 );

    void print( size_t id );

    void unique( size_t id );

    void redirect_to_file();
};


上一章:输入输出和模板
下一章:

8. 参考资料

  1. C++程序设计
  2. pixiv illustration: 夏日轨道

9. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值