数据结构——数组

数组

github地址

数组基础

  • 数组最大的有点:快速查询。索引快
  • 数组最好应用于 “索引有语义” 的情况
  • 但并非所有有语义的索引都适用于数组(身份证号)
  • 数组也可以处理 ”索引没有语义“ 的情况

封装数组类

  • 数组类该具备的功能:增 删 改 查

使用泛型:

  • 让我们的数据结构可以放置 “任何” 数据类型

实现

构造函数

我们希望有两个构造函数,可以支持给定容量和默认容量,这里默认容量我们设置成 10 :

template<typename T>
MyArray<T>::MyArray() {
  size_ = 0;
  capacity_ = 10;
  data_ = new T[capacity_];
  std::cout << "调用 MyArray() 构造." << std::endl;
}

template<typename T>
MyArray<T>::MyArray(int capacity) {
  if (capacity <= 0) {
    std::cout << "MyArray(int) error. Capacity is illegal." << std::endl;
    throw "MyArray(int) error. Capacity is illegal.";
  }
  size_ = 0;
  capacity_ = capacity;
  data_ = new T[capacity_];
  std::cout << "调用 MyArray(int capacity) 构造." << std::endl;
}

析构函数

template<typename T>
MyArray<T>::~MyArray() {
  if (nullptr != data_) {
    delete[] data_;
    data_ = nullptr;
  }
  size_ = 0;
  capacity_ = 0;
  std::cout << "调用 ~MyArray() 析构." << std::endl;
}

拷贝构造和拷贝赋值

首先添加拷贝构造测试:

// 初始化
class ArrayTest : public testing::Test {
 protected:
  void SetUp() override {
    for (int i = 0; i < 10; ++i) {
      my_array_.AddLast(i);
    }
  }
  void TearDown() override {
  }
  MyArray<int> my_array_;
};

// 拷贝构造测试
TEST_F(ArrayTest, CopyConstructorTest) {
  MyArray<int> my_array(my_array_);
  EXPECT_EQ(10,my_array.GetSize());
  EXPECT_EQ(9,my_array.Get(9));
}

这里出现了 TEST_F,在TDD实例有例子。

拷贝构造函数和赋值函数:

template<typename T>
MyArray<T>::MyArray(const MyArray &arr) {
  this->size_     = arr.size_;
  this->capacity_ = arr.capacity_;
  this->data_     = new T[capacity_];
  for (int i = 0; i < size_; ++i) {
    this->data_[i] = arr.data_[i];
  }
  std::cout << "调用  MyArray(const MyArray &arr) 拷贝构造" << std::endl;
}

template<typename T>
MyArray<T> &MyArray<T>::operator=(const MyArray<T> &arr) {
  assert(this != &arr);
  if (this->data_ != nullptr) {
    delete[] this->data_;
    this->data_  = nullptr;
  }
  //分配内存
  this->size_     = arr.size_;
  this->capacity_ = arr.capacity_;
  this->data_     = new T[capacity_];
  //拷贝数据
  for (int i = 0; i < size_; i++) {
    //如果是自定义的复杂数据类型,必须对 = 运算赋进行重载,  operator=
    this->data_[i] = arr.data_[i];
  }

  std::cout << "调用 = 赋值操作 " << std::endl;
  return* this;
}

移动构造和移动赋值

首先添加测试用例:

// 移动构造和移动赋值测试
TEST_F(ArrayTest, MoveTest) {
  // 移动拷贝构造
  MyArray<int> my_array(std::move(my_array_));

  // 移动赋值传入本身 会报错
//  my_array = std::move(my_array);
//  EXPECT_EQ(10,my_array.GetSize());
//  EXPECT_EQ(9,my_array.Get(9));

  MyArray<int> my_array1;
  // 移动赋值
  my_array1 = std::move(my_array);
  EXPECT_EQ(10,my_array1.GetSize());
  EXPECT_EQ(0,my_array.GetSize());
  EXPECT_EQ(0,my_array_.GetSize());
}

移动构造和移动赋值:

template<typename T>
MyArray<T>::MyArray(MyArray &&arr) noexcept {
  assert(this != &arr);
  capacity_ = std::exchange(arr.capacity_, 0);
  size_     = std::exchange(arr.size_, 0);    // 非类类型成员的显式移动
  data_     = std::move(arr.data_);           // 类类型成员的显式移动
  arr.data_ = nullptr;
  std::cout << "调用  MyArray(const MyArray &&arr) 移动构造函数" << std::endl;
}

template<typename T>
MyArray<T> &MyArray<T>::operator=(MyArray<T> &&arr) noexcept {
  assert(this != &arr);
  if (this->data_ != nullptr) {
    delete[] this->data_;
    this->data_ = nullptr;
  }
  //分配内存
  this->size_ = std::exchange(arr.size_, 0);
  this->capacity_ = std::exchange(arr.capacity_, 0);
  this->data_ = new T[capacity_];
  //拷贝数据
  for (int i = 0; i < size_; i++) {
    //如果是自定义的复杂数据类型,必须对 = 运算赋进行重载,  operator=
    this->data_[i] = std::move(arr.data_[i]);
  }

  std::cout << "调用 = 移动赋值操作 " << std::endl;
  return * this;
}

<< 重载

测试用例:

// 测试 << 重载
TEST_F(ArrayTest, OperatorTest) {
  std::cout << my_array_ << std::endl;
}

实现:

// 内部友元函数实现 重载输出 << 操作符,这里必须定义成友元
friend std::ostream &operator<<(std::ostream &out, MyArray<T> &obj) {
  out << "MyArray size = " << obj.size_ << ", Capacity = " << obj.capacity_ << std::endl;
  out << "MyArray: [";
  for (int i = 0; i < obj.size_; ++i) {
    out << obj.data_[i];
    if (i != obj.size_ - 1)
      out << ", ";
  }
  out << "] ";

  return out;
}

[] 重载

测试用例:

// [] 测试
TEST_F(ArrayTest, SquareBracketsTest) {
  EXPECT_EQ(0,my_array_[0]);
  EXPECT_EQ(1,my_array_[1]);
}

实现:

template<typename T>
T &MyArray<T>::operator[](int index) {
  if (index < 0 || index >= size_) {
    std::cout << "[] fail. Index is illegal." << std::endl;
    throw "[] fail. Index is illegal.";
  }
  std::cout << "[] 调用" << std::endl;
  return this->data_[index];
}

添加元素

首先添加测试:

TEST(ArrayTestNoF, AddAndGet) {
  MyArray<int> my_array;
  my_array.Add(0,1);
  my_array.Add(1,2);
  my_array.Add(2,3);
  EXPECT_EQ(1,my_array.Get(0));
  EXPECT_EQ(2,my_array.Get(1));
  EXPECT_EQ(3,my_array.Get(2));
  my_array.AddFirst(4);
  EXPECT_EQ(4,my_array.Get(0));
  EXPECT_EQ(4,my_array.GetFirst());
  my_array.AddLast(5);
  EXPECT_EQ(5,my_array.Get(4));
  EXPECT_EQ(5,my_array.GetLast());
}

我们希望提供三个接口:在指定位置插入元素,在头部和尾部插入元素

后两个方法可以通过第一个方法实现,我们先实现第一个方法,如下:

template<typename T>
void MyArray<T>::Add(int index, T t) {
  if (index < -1 || index > size_) {
    std::cout << "Add fail. Index is illegal." << std::endl;
    throw "Add fail. Index is illegal.";
  }
  if (IsFull()) {  
    Resize(capacity_ * 2);
  }
  for (int i = size_; i > index; --i) {
    data_[i] = data_[i - 1];
  }
  data_[index] = t;
  size_++;
}

后两个方法在此基础上:

template<typename T>
void MyArray<T>::AddFirst(T t) {
  Add(0, t);
}

template<typename T>
void MyArray<T>::AddLast(T t) {
  Add(size_, t);
}

判断空满

测试用例:

TEST(ArrayTestNoF, IsEmpty) {
  MyArray<int> my_array(10);
  EXPECT_TRUE(my_array.IsEmpty());
}
template<typename T>
bool MyArray<T>::IsFull() const {
  return size_ == capacity_;
}

template<typename T>
bool MyArray<T>::IsEmpty() const {
  return size_ == 0;
}

获取容量和大小

测试用例:

TEST_F(ArrayTest, GetSizeAndGetCapacity) {
  EXPECT_EQ(10,my_array_.GetCapacity());
  EXPECT_EQ(10,my_array_.GetSize());
}

实现:

template<typename T>
int MyArray<T>::GetSize() const {
  return size_;
}

template<typename T>
int MyArray<T>::GetCapacity() const {
  return capacity_;
}

对指定位置元素赋值

测试用例:

TEST_F(ArrayTest, Set) {
  my_array_.Set(0,11);
  EXPECT_EQ(11,my_array_.GetFirst());
}

实现:

template<typename T>
void MyArray<T>::Set(int index, T t) {
  if (index < 0 || index >= size_) {
    std::cout << "Set Fail. Index is illegal." << std::endl;
    throw "Set fail. Index is illegal.";
  }
  if (IsEmpty()) {
    std::cout << "Set Fail. Array is empty." << std::endl;
    throw "Set fail. Array is empty.";
  }
  data_[index] = t;
}

查找元素

测试用例:

TEST_F(ArrayTest, FindAndContain) {
  EXPECT_EQ(0,my_array_.Find(0));
  EXPECT_TRUE(my_array_.Contain(1));
  EXPECT_FALSE(my_array_.Contain(111));
}

实现:

template<typename T>
int MyArray<T>::Find(T t) const {
  if (IsEmpty()) {
    std::cout << "Find fail. Array is empty." << std::endl;
    throw "Find Fail. Array is empty";
  }
  for (int i = 0; i < size_; ++i) {
    if (data_[i] == t) {
      return i;
    }
  }
  return -1;
}

template<typename T>
bool MyArray<T>::Contain(T t) const {
  return Find(t) != -1;
}

调整空间

测试用例:

TEST_F(ArrayTest, Resize) {
  EXPECT_EQ(10,my_array_.GetSize());
  EXPECT_EQ(10,my_array_.GetCapacity());
  my_array_.AddLast(10);
  EXPECT_EQ(11,my_array_.GetSize());
  EXPECT_EQ(20,my_array_.GetCapacity());
  for (int j = 0; j < 6; ++j) {
    my_array_.RemoveLast();
  }
  EXPECT_EQ(5,my_array_.GetSize());
  EXPECT_EQ(20,my_array_.GetCapacity());
  my_array_.RemoveLast();
  EXPECT_EQ(4,my_array_.GetSize());
  EXPECT_EQ(10,my_array_.GetCapacity());
}

全部的源码传送门

时间复杂度分析

  • 添加操作       O(n)
    • addLast(e)      O(1)
    • addFirst(e)      O(n)
    • add(index,e)   O(n/2)=O(n)

最坏情况:O(n) , resize() 时间复杂度 O(n)

  • 删除操作 O(n)
    • removeLast()     O(1)
    • removeFirst()     O(n)
    • remove(index)    O(n/2)=O(n)

最坏情况:O(n) , resize() 时间复杂度 O(n)

  • 修改操作 O(1)

    • set(index,e)    O(1)
  • 查找操作

    • get(index)    O(1)
    • contains(e)  O(n)
    • find(e)           O(n)

均摊复杂度分析

由于 resize() O(n) 的存在,所以每次 addLastremoveLast 时间复杂度依然是 O(n) ?

resize()复杂度分析

假设当前 capacity 为 8 ,并且每次添加操作都是 addLast , 9 次添加操作触发一次 resize(), 总共进行了 9 次基本操作, n 次操作才会触发一次时间复杂度为 O(n) 的操作,平摊到前 n 次操作,相当于前 n 次操作每次复杂度为 O(1+1)=O(1)

在这个例子中,这样均摊计算,比计算最坏情况更有意义。

但是会出现这么一种情况:

复杂度振荡

size 为 n 时,我们先后执行 addLast , removeLast , addLast , removeLast , 每一次的操作时间复杂度都是 O(n) 的,出现问题的原因在于 removeLastresize 过于着急 (Eager)

解决方案:Lazy

size == capacity / 4 时,才将 capacity 减半。

测试是否有内存泄露

使用 valgrind 测试测试用例是否存在内存泄露

valgrind --leak-check=full --show-reachable=yes --trace-children=yes ./ArraysTest

使用 Valgrind 分析 C++ 程序时,有一些问题需要留意。例如,这个程序并没有发生内存泄漏,但是从 HEAP SUMMARY 可以看到,程序分配了 303 次内存,但却只释放了 303 次内存,为什么会这样呢?
  实际上这是由于 C++ 在分配内存时,为了提高效率,使用了它自己的内存池。当程序终止时,内存池的内存才会被操作系统回收,所以 Valgrind 会将这部分内存报告为 reachable 的,需要注意,reachable 的内存不代表内存泄漏,例如,从上面的输出中可以看到,有 72704 个字节是 reachable 的,但没有报告内存泄漏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值