动态数组(vector)-详解均摊复杂度分析和避免避免复杂度的震荡(C++实现)

(1)首先我们先写出MyVector类的基本框架

template <typename T>
class MyVector{

private:
    T* data;
    int size;       // 存储数组中的元素个数
    int capacity;   // 存储数组中可以容纳的最大的元素个数

public:
    MyVector(){

        data = new T[100];
        size = 0;
        capacity = 100;
    }

    ~MyVector(){

        delete[] data;
    }
}

(2)为数组中添加一个元素

    void push_back(T e){

        assert( size < capacity );

        data[size++] = e;
    }

(3)从数组中拿出一个元素

    // 平均复杂度为 O(1)
    T pop_back(){

        assert(size > 0);
        
        size --;

        return data[size];
    }

以上便实现了一个静态数组,接下来,我们修改一下代码,实现动态数组

(4)当添加元素的时候,将数组的容量扩容,扩容的大小为原来的两倍

    // 平均复杂度为 O(1)
    void push_back(T e){

        if( size == capacity )
            resize( 2* capacity );

        data[size++] = e;
    }

这首扩容不可以是一个定值,应该选择它的最小倍,即2 *容量

(5)实现调整大小()函数

    // 复杂度为 O(n)
    void resize(int newCapacity){

        assert(newCapacity >= size);//新添加的容量应该能够完全容纳现有的容量
        T *newData = new T[newCapacity];
        for(int i = 0 ; i < size ; i ++)
            newData[i] = data[i];
        delete[] data;

        data = newData;
        capacity = newCapacity;
    }

在pop_back函数中是不是也可以用同样的方法缩容呢?答案是否定的,这时候涉及到均摊复杂度分析

这时候再添加两个元素,数组就会扩容,然后将现有数组拷贝,需要复杂度Ñ

 前Ñ次添加元素需要耗费N,当在1/2容量出在添加一次元素需要耗费N,总共花费2

在pushback()函数中,只有大小== capacity时,才会触发resize(),这时候将每次扩容时候所耗费的时间均匀到为调用resize()的时间中,这便是均摊复杂度。

所以oush_back平均复杂度是O(1)。

这时候可以做一个实验,验证我们的想法。

(6)的的push_back复杂度的验证

#include <iostream>
#include <cassert>
#include <cmath>
#include <ctime>
#include "MyVector.h"

using namespace std;

int main() {

	for (int i = 10; i <= 26; i++) {

		int n = pow(2, i);

		clock_t startTime = clock();
		MyVector<int> vec;
		for (int num = 0; num < n; num++)
			vec.push_back(i);

		clock_t endTime = clock();

		cout << 2 * n << " operations: \t";
		cout << double(endTime - startTime) / CLOCKS_PER_SEC << " s" << endl;
	}
	system("pause");
	return 0;
}

 运行程序:

 可以看到,后一个是前一个的两倍

(7)关于pop_back(),却需要做一下修改

按照添加一个元素的扩容方法,可能我们会这么做:

    T pop_back(){

        assert(size > 0);

        size --;


        if(size == capacity / 2)
            resize(capacity / 2);

        return data[size];
    }

第一点:这时候代码有一个小的错误,当size--后,缩容之后,数据[大小]这个元素已经被抹掉,这时候应该先保存:

    // 平均复杂度为 O(1)
    T pop_back(){

        assert(size > 0);
        T ret = data[size-1];
        size --;

        if(size == capacity / 2)
            resize(capacity / 2);

        return ret;
    }

第二点:会发生复杂度震荡:

(1)每删除一个元素的复杂度为O(1)

(2)当元素只剩下Ñ个是,需要进行一次的调整大小操作

 (3)调整操作:

(4)此时,时间复杂度为n + 1个,其中1为调整大小操作需花费的时间 

(5)做均摊分析,可知此时平均复杂度为O(1)。

(6)此时,如果在这个临界点发生添加和删除震荡,无法均摊...

复杂度变为0(n)的 

(7)所以,应该在容量为1/4的时候才进行大小调整

(8)此时,如果添加一个元素,需要调整大小,时间复杂度为n;

若有删除一个元素,此时容量为n,又会发生一次resize,时间复杂度为n,

这时候为再添加一个元素时,留出了空间:

(9)这时候,只需稍微修改一下代码:

    // 平均复杂度为 O(1)
    T pop_back(){

        assert(size > 0);
        T ret = data[size-1];
        size --;

        // 在size达到静态数组最大容量的1/4时才进行resize
        // resize的容量是当前最大容量的1/2
        // 防止复杂度的震荡
        if(size == capacity / 4)
            resize(capacity / 2);

        return ret;
    }

(10) 到这里,我们就完成了一个动态数组的类。

以下是类的完整实现:

template <typename T>
class MyVector{

private:
    T* data;
    int size;       // 存储数组中的元素个数
    int capacity;   // 存储数组中可以容纳的最大的元素个数

    // 复杂度为 O(n)
    void resize(int newCapacity){

        assert(newCapacity >= size);//新添加的容量应该能够完全容纳现有的容量
        T *newData = new T[newCapacity];
        for(int i = 0 ; i < size ; i ++)
            newData[i] = data[i];
        delete[] data;

        data = newData;
        capacity = newCapacity;
    }

public:
    MyVector(){

        data = new T[100];
        size = 0;
        capacity = 100;
    }

    ~MyVector(){

        delete[] data;
    }

    // 平均复杂度为 O(1)
    void push_back(T e){

        if( size == capacity )
            resize( 2* capacity );

        data[size++] = e;
    }

    // 平均复杂度为 O(1)
    T pop_back(){

        assert(size > 0);
        T ret = data[size-1];
        size --;

        // 在size达到静态数组最大容量的1/4时才进行resize
        // resize的容量是当前最大容量的1/2
        // 防止复杂度的震荡
        if(size == capacity / 4)
            resize(capacity / 2);

        return ret;
    }

};

 

参考资料:

https://coding.imooc.com/class/chapter/82.html#Anchor

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++中,vector动态申请内存的过程是通过调用new操作符实现的。当vector中元素数量超过当前内存空间大小时,会重新申请更大的内存空间,并将原有元素复制到新的内存空间中。 下面是一个用C实现类似vector的简单示例代码,实现了动态增加元素、获取元素数量、获取指定位置的元素等功能: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { int *data; // 存储元素的内存空间 int size; // 当前元素数量 int capacity; // 当前内存空间大小 } Vector; void vector_init(Vector *vec) { vec->data = NULL; vec->size = 0; vec->capacity = 0; } void vector_push_back(Vector *vec, int value) { // 如果当前元素数量已经等于内存空间大小,需要重新申请更大的内存空间 if (vec->size == vec->capacity) { int new_capacity = vec->capacity ? vec->capacity * 2 : 1; int *new_data = (int*)realloc(vec->data, new_capacity * sizeof(int)); if (!new_data) { printf("error: out of memory\n"); exit(1); } vec->data = new_data; vec->capacity = new_capacity; } // 在数组末尾添加新元素 vec->data[vec->size++] = value; } int vector_size(const Vector *vec) { return vec->size; } int vector_at(const Vector *vec, int index) { if (index < 0 || index >= vec->size) { printf("error: index out of range\n"); exit(1); } return vec->data[index]; } int main() { Vector vec; vector_init(&vec); for (int i = 0; i < 10; i++) { vector_push_back(&vec, i); } for (int i = 0; i < vector_size(&vec); i++) { printf("%d ", vector_at(&vec, i)); } printf("\n"); return 0; } ``` 在上面的示例代码中,我们使用realloc函数来重新申请更大的内存空间。如果申请失败,则会打印错误信息并退出程序。vector_push_back函数用于向数组末尾添加新元素,如果当前元素数量已经等于内存空间大小,则会调用realloc函数申请更大的内存空间。vector_size函数用于获取当前元素数量,vector_at函数用于获取指定位置的元素。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值