vector的insert方法以及合并排序的数组

58 篇文章 1 订阅

结合LeetCode每日一题活动题目以及最近看《Effective STL》的体会,总结一下关于vector容器的插入问题

先来看题:

给定两个排序后的数组 A 和 B,其中 A 的末端有足够的缓冲空间容纳 B。 编写一个方法,将 B 合并入 A 并排序。

初始化 A 和 B 的元素数量分别为 m 和 n。

示例:

输入:
A = [1,2,3,0,0,0], m = 3
B = [2,5,6], n = 3

输出: [1,2,2,3,5,6]

问题比较简单,一般有两种方法:

  1. 合并排好序的数组最容易想到归并。即每次比较两个数组的头元素,将小的那个存入到第三个数组中,依次这样操作直到两个数组完全被合并。归并的优点是当新的元素插入到新的数组后,不需要进行元素移动等相关操作,因为新元素插入的位置一直是新数组的尾端。缺点是新数组占用了内存消耗,可能还要进行额外的拷贝,这种方法的时间复杂度为 O ( n + m ) O(n+m) O(n+m)n和m分别为两个数组的长度。
  2. 将一个数组插入到另一个数组中,这种方法的优点是不需要创建额外的新数组,缺点是要在被插入的数组中留够足够的内存空间,并且每次插入要使用一个逆序指针来移动插入位置之后的所有元素,这就将时间复杂度为提高到了 O ( N 2 ) O(N^2) O(N2)

代码:

class Solution {
    //法1:归并
public:
    void merge(vector<int>& A, int m, vector<int>& B, int n) {
        vector<int> ans;
        int indexA = 0, indexB = 0;
        while (indexA < m || indexB < n){
            if (indexA >= m || (indexB < n && A[indexA] > B[indexB])){
                ans.push_back(B[indexB++]);
            }
            else{
                ans.push_back(A[indexA++]);
            }
        }
        A.clear();
        A = ans;
    }
};

class Solution {
    //法2:逆向双指针
public:
    void merge(vector<int>& A, int m, vector<int>& B, int n) {
        int index = 0;
        int lenA = A.size();
        for (int i = 0; i < n; i++){
            while (index < m && A[index] <= B[i]){
                index++;
            }
            for (int j = lenA- 1; j > index; j--){
                A[j] = A[j - 1];
            }
            A[index++] = B[i];
            m++;
        }
    }
};

接着这个问题,顺便总结一下C++ STL的vector里insert操作常用的注意事项。
vector中的insert有三种用法:
1.在指定位置loc前插入值为val的元素,返回指向这个元素的迭代器;
2.在指定位置loc前插入num个值为val的元素;
3.在指定位置loc前插入区间[start, end)的所有元素;
iterator insert( iterator loc, const TYPE &val );
void insert( iterator loc, size_type num, const TYPE &val );
void insert( iterator loc, input_iterator start, input_iterator end );
就效率方面来说,区间成员函数应该优于与之对应的单元素成员函数。

比如,假定你要把一个int数组拷贝到一个vector的前端。使用vector的区间insert函数,非常简单:

int data[numValues]; //假定numValues在别处定义
vector<int> v;
...
v.insert(v.begin(), data, data + numValues); //把整数插入到v的前端

如果显示地调用单元素插入,则应该写成这样:

vector<int>::iterator insertLoc(v.begin());
for (int i = 0; i < numValues; i++){
	insertLoc = v.insert(insertLoc, data[i]);
	++insertLoc;
}

在进行这样的insert操作时,必须要把insert的返回值记下来供下次进入循环时使用。如果在每次插入操作后不更新insertLoc,我们会遇到两个问题。首先,第一次迭代后的所有循环迭代都将导致不可预料的行为(undefined behavior),因为每次调用insert都会使insertLoc无效。其次,即使insertLoc仍然有效,插入总是发生在vector的最前面(即在v.begin()处),结果这组整数被以相反的顺序拷贝到v中。

相比于使用区间形式的insert插入,使用单元素版本的insert总共在三个方面影响了效率:

  1. 第一种影响是不必要的函数调用。把numValues个元素逐个插入到v中导致了对insert的numValues次调用。而使用区间形式的insert,则只做了一次函数调用,节省了numValues-1次。当然,使用内联函数可能会避免这样的影响,但是,实际中不见得会使用内联函数。只有一点是肯定的:使用区间形式的insert,肯定不会有这样的影响。
  2. 内联无法避免第二种影响,即把v中已有的元素频繁移动到插入后它们所处的位置。每次调用insert把新元素插入到v中时,插入点后的每个元素都要向后移动一个位置,以便为新元素腾出空间。所以,位置p的元素必须被移动位置p+1。我们向v的前端插入numValues个元素,这意味着v中插入点之后的每个元素都要向移动numValues个位置。每次调用insert时,每个元素需要向后移动一个位置,所以每个元素将移动numValues次。如果插入前v中有n个元素,就会有nnumValues次移动(对于插在begin位置这种情况来说)。在这个例子中,v中存储的是int类型,每次移动最终可能会归为调用memmove,可是如果v中存储的是用户自定义类型,则每次移动会导致调用该类型的赋值操作符拷贝构造函数(大多数情况下会调用赋值操作符,但每次vector中的最后一个元素被移动时,将会调用该元素的拷贝构造函数)。所以,在通常情况下,把numValues个元素逐个插入到含有n个元素的vector前端将会有nnumValues次函数调用的代价:(n-1)*numValues次调用Widget的赋值操作符合numValues次调用Widget的拷贝构造函数。即使这些调用是内联的,你仍然需要把v中的元素移动numValues次。与此不同的是,C++标准要求区间insert函数把现有容器中的元素直接移动到他们最终的位置上,即只需付出每个元素移动一次的代价。总的代价包括n次移动、numValues次调用该容器中元素类型的拷贝构造函数,以及调用该类型的赋值操作符。同每次插入一个元素的策略相比较,区间insert减少了n * (numValues - 1) 次移动。
  3. 如果试图把一个元素插入到vector中,而它的内存已满,那么vector将分配具有更大容量的新内存,把它的元素从旧内存拷贝到新内存中,销毁旧内存中的元素,并释放旧内存。然后它把要插入的元素加入进来。对应的,使用区间插入的方法,在开始插入前可以知道自己需要多少新内存,所以不必多次重新分配vector的内存。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值