从零开始实现数据结构(二) 有序数组
- 2019 年 10 月 3 日
- 筆記
有序数组顾名思义就是数组内所有元素的排列都是按照一定的顺序的。不管插入元素是多少,有序数组都会自动将该元素插入合适的位置。这里要实现的是一个定长的有序数组。
一.有序数组COrderArray类
要实现一个有序数组,首先应该考虑这个数组应该要实现什么样的功能:
1.构造函数,初始化数组,给定指定的数组长度。
2.插入数据,最重要的功能,插入的数据自动按照顺序排好。
3.按照指定值查找,有序数组有一个很重要的特点就是很方便查找某一个特定值。
4.按照指定的位置查找,这个功能也相当于为重载 [] 下标运算符,这样就可以像普通的数组一样通过下标访问有序数组的元素。
5.删除指定位置的元素。
6.删除指定值的元素。
7.清空所有元素。
8.合并两个有序数组,这个功能也是刷算法题可能会遇到的一种类型的题目。
理清楚大概要实现什么样的功能,就可以开始写头文件的函数声明。
#pragma once #include "CArray.h" class COrderArray ://依然是继承数组基类 public CArray { public: COrderArray(int size);//构造函数 void PushBack(int value);//插入数据 void RemoveForValue(int value);//根据指定值删除数组元素 void RemoveForPos(int pos);//根据指定位置删除数组元素 int FindForValue(int value);//根据指定值查找数组元素 int &operator[](int pos); //重载下标运算符,达到按位置操作数组元素的目的 //重载下标运算符的时候,这里的返回值必须是引用类型 void Clear();//清空所有元素 void Merge(COrderArray& array);//合并两个有序数组 private: void _InsertElement(int value, int start); //因为在数组中插入值的时候会将该位置后的所有的元素都向后移一个位置,参数为插入值和后移的初始位 //所以单独写一个私有函数来完成插入并后移的功能,防止函数体内容太多,该函数也是不能在外部去调用 void _RemoveElement(int pos); //和上面同理,这个函数是用来删除数组中某个位置的元素,删除完成后把该位置后面的元素前移 //注意:这两个函数和上面的的插入、删除函数的不同之处在于上面两个公有函数负责给用户调用 //并且实现用什么样的策略去删除或者插入,它们是内部调用下面的两个私有函数 //而这两个私有函数只是是完成插入和删除的功能 };
其他的成员函数以及成员变量在CArray数组基类中已经定义了,这里就不用再写。
二.COrderArray类的具体实现
1.构造函数
因为这是一个定长的有序数组,所以构造函数需要给定一个n,然后把数组的容量设置为n
COrderArray::COrderArray(int size) { m_Capacity = size; mp_Array = new int[m_Capacity]; }
2.插入元素
有序数组每插入一个元素都要考虑插入的位置,确保插入以后整个数组依然是有序的。所以插入元素以前要把当前要插入的元素和数组已经有的元素大小进行比较。可以采用顺序比较的方法,即从最小的元素开始比较。
但是因为这是一个有序数组,顺序比较的方式会显得很笨重,在这里使用二分法是最好的。即首先确定一个初始位和末尾位,然后两个位置中间的元素。如果要插入的元素比中间的元素大,则把起始位重新设置为现在的中间位置、,然后重新计算新的中间位,再比较中间位。这样的好处是很快就能找到合适的插入位置。
//插入元素,判断元素大小来决定其应该在数组中位置 void COrderArray::PushBack(int value) { //数组元素个数已经达到数组最大容量,直接返回 if (m_Size == m_Capacity) { return; } //二分查找 int start = 0; int end = m_Size; int mid = (start + end) / 2; while (1) { //数组还没有元素的时候,直接插入 if (m_Size == 0) { mp_Array[m_Size] = value; break; } //循环跳出的条件,即mid与首尾之一相等 if (mid==start||mid==end) { if (mp_Array[mid] > value)//如果最后一次循环时,mid位置的元素大于插入位置的元素 { _InsertElement(value, mid);//对当前mid位置,调用插入元素的函数 break; } //若最后一次循环时,mid位置元素小于插入位置的元素 //对mid位置后面的一个位置,调用插入元素的函数 _InsertElementvalue, mid + 1); break; } //二分法,比较插入元素和中间元素的大小,然后改变mid的值 if (mp_Array[mid] > value)//插入元素比中间元素小,end改为mid,mid根据新的end更新一下 { end = mid; mid = (start + end) / 2; } else//插入元素比中间元素大,start改为mid,mid根据新的start更新一下 { start = mid; mid = (start + end) / 2; } } m_Size++; }
二分法的过程比较适合在纸上一步一步画出来,就很容易理解这个过程。
在刚刚的函数中只是实现了去搜索插入元素应该插入的位置,实际上还并没有完成插入,所以在循环结束的时候还调用了_InsertElement(value, mid),来完成插入,并把插入位置后面的所有元素都后移一位。
**_InsertElement函数如下:**
//在数组中要插入的元素位置之后的所有元素进行后移 void COrderArray::_InsertElement(int value, int pos) { for (int i = m_Size; i > pos; i--) { mp_Array[i] = mp_Array[i - 1]; } mp_Array[pos] = value; }
这里只需要根据插入的位置,一个for循环把后面所有的元素移动一位,再插入新的元素即可。
3.根据指定值查找元素
根据值查找元素和上面的方法类似,使用二分查找。
//根据值查找,二分查找,和上面的插入类似 int COrderArray::FindForValue (int value) { int start = 0; int end = m_Size; int mid = (start + end) / 2; while (mid != start && mid != end) { if (mp_Array[mid] == value) { return mid; } if (mp_Array[mid] > value) { end = mid; mid = (start + end) / 2; } else { start = mid; mid = (start + end) / 2; } } if (mp_Array[mid] == value) { return mid; } }
4.根据指定位置查找元素
这一个功能实际上和数组的下标索引是相同的,所以这个地方可以不写函数,而是重载下标运算符。之前写的动态数组也可以加入这一个功能。
int &COrderArray::operator[](int pos) { if (pos > m_Size || pos < 0) { return mp_Array[0]; } return mp_Array[pos]; }
重载下标运算符的时候返回值是一个引用类型,而且最好是做一个越界的检查,如果下标索引越界,就返回数组的第一个元素。
5.清空所有元素
和动态数组清空元素类似,只需要把当前元素个数置零即可。
//清空所有元素 void COrderArray::Clear() { m_Size = 0; }
6.根据指定位置删除元素
根据指定位置删除数组元素只需要把要删除的位置后面的所有元素都向前移动一个位置,这样就自动覆盖了要删除的元素。而前面写的_RemoveElement(int pos)函数功能就是将删除元素,并指定位置后的元素向前移动,所有这里可以直接考虑调用这个函数。
**_RemoveElement(int pos)如下:**
//删除操作,把删除的数后面每一个元素向前移动一个位置 void COrderArray::_RemoveElement(int pos) { if (pos >= 0 && pos < m_Size) { for (int i = pos; i < m_Size; i++) { mp_Array[i] = mp_Array[i + 1]; } m_Size--; } }
根据指定位置删除元素的函数:
//根据指定位置删除元素 void COrderArray::RemoveForPos(int pos) { _RemoveElement(pos); }
7.根据指定值删除元素
要根据指定值删除数组中的元素,首先其实还是要查找到数组中指定值,所以可以直接用前面写的查找指定值的函数,用一个变量来接收返回的位置,找到以后再调用前面的的_RemoveElement(int pos)函数即可。
//根据指定值删除元素 void COrderArray::RemoveForValue(int value) { int pos = FindForValue(value); _RemoveElement(pos); }
8.合并两个有序数组
要实现两个有序数组的合并,有几种思考方式,一种是使用一个新的对象来装合并后的有序数组,这种开销相对比较大。所以这里选择将第一个有序数组和第二个有序数组都合并到第一个里面,并且不改变第二个数组。所以函数的参数就应该是第二个有序数组。
即 Merge(COrderArray& tempArray)
注意这里使用的是COrderArray&,这是一个引用。当类对象做函数形参的时候通常都会使用引用的方式,因为使用引用不会调用拷贝构造函数,而是直接操作原来的对象,这样可以减轻程序的开销。
同时,如果在对象中包含有new出来的指针,一定得要使用引用的方式传参。因为值传递会生成一个对象的副本,在结束的时候还会自动执行析构函数,会把原来对象new出来的指针delete掉。而在整个程序结束时,又会调用一次析构函数,这样重复delete同一个指针,会运行出错。而引用作参数则不会生成对象的副本,也不会调用析构函数。当然,如果对象本身没有动态创建的变量,值传递也不会出太大的问题。
合并的思想主要是两个数组依次比大小,小的就先放进新的数组中,然后继续比下去。
因为这里创建的是一个定长数组,所以得先开一个新的更大的内存空间,大小就为两个数组容量之和,所以这里的合并从后往前合并应该是更好的。
//合并两个有序数组 void COrderArray::Merge(COrderArray& tempArray) { //开一个新的内存来存储合并后的数组,首先需要计算新的内存需要多大 int tempArrayCapacity = tempArray.getCapacity(); int newCapacity = tempArrayCapacity + m_Capacity; int* newSpace = new int[newCapacity]; //因为原来数组的元素有可能没有满 //所以要从两个数组当前size的位置开始合并 //tempsize即两个数组的size之和 //pos是合并的时候的一个索引,每取出来一个元素,就放到newSpace[pos],并-1,到0的时候就合并完 int tempArraySize = tempArray.getSize(); int tempSize = tempArraySize + m_Size; int pos = tempSize - 1;//索引要-1 //开始合并 while (pos >= 0) { //如果第一个数组元素个数不为0才进行比较 if (m_Size != 0) { //第一个数组元素大于传参进来的数组元素 //或者第二个数组元素索引已经比较到0 //第一个数组的元素放进新开的内存空间,然后size-1 if (mp_Array[m_Size - 1] >= tempArray[tempArraySize - 1]) { newSpace[pos] = mp_Array[m_Size - 1]; m_Size--; } else { newSpace[pos] = tempArray[tempArraySize - 1]; tempArraySize--; } }else//如果第一个数组元素个数已经为0,直接把另一个数组的元素按顺序放进来,不需要再进行比较 { newSpace[pos] = tempArray[tempArraySize - 1]; tempArraySize--; } pos--; } //清理掉要合并数组的原来的内存 //把size设置为新的合并后的size //数组容量设置为新的容量 //指针要指向新的存放数据的内存空间 delete[] mp_Array; m_Size = tempSize; m_Capacity = newCapacity; mp_Array = newSpace; }
到这里,所有的有序数组的功能就实现完毕,接下来是主函数来调用这个有序数组。
三.主函数Array.cpp
主函数主要是创建两个有序数组对象,然后插入数据,可以看到插入的数据都是有序的,然后再合并两个有序数组。
#include<iostream> using namespace std; #include "COrderArray.h" using namespace std; int main() { int n; cin >> n; //COrderArray* oArray = new COrderArray(n); COrderArray oArray(n); COrderArray oArray2(n); //插入第一个数组的数据 for (int i = 0; i < n; i++) { int temp; cin >> temp; oArray.PushBack(temp); } //插入第二个数组的数据 for (int i = 0; i < n; i++) { int temp; cin >> temp; oArray2.PushBack(temp); } oArray.Print();//打印数组 oArray.Merge(oArray2);//合并两个数组 oArray.Print();//重新打印 return 0; }
输入数据: 6 9 4 6 11 5 8 7 10 6 18 2 4 输出数据: print all date : 4 5 6 8 9 11 合并数组后的输出数据: print all date : 2 4 4 5 6 6 7 8 9 10 11 18
这样就实现了一个有序数组就,这个地方和前面一样也只是int作为数组的类型,并没有使用模板,所以还可以扩展开来,实现更多的功能。最后给出所有文件的代码。
所有文件源代码
COrderArray.h文件的全部代码如下:
#pragma once #include "CArray.h" class COrderArray ://依然是继承数组基类 public CArray { public: COrderArray(int size);//构造函数 void PushBack(int value);//插入数据 void RemoveForValue(int value);//根据指定值删除数组元素 void RemoveForPos(int pos);//根据指定位置删除数组元素 int FindForValue(int value);//根据指定值查找数组元素 int &operator[](int pos); //重载下标运算符,达到按位置操作数组元素的目的 //重载下标运算符的时候,这里的返回值必须是引用类型 void Clear();//清空所有元素 void Merge(COrderArray& array);//合并两个有序数组 private: void _InsertElement(int value, int start); //因为在数组中插入值的时候会将该位置后的所有的元素都向后移一个位置,参数为插入值和后移的初始位 //所以单独写一个私有函数来完成插入并后移的功能,防止函数体内容太多,该函数也是不能在外部去调用 void _RemoveElement(int pos); //和上面同理,这个函数是用来删除数组中某个位置的元素,删除完成后把该位置后面的元素前移 //注意:这两个函数和上面的的插入、删除函数的不同之处在于上面两个公有函数负责给用户调用 //并且实现用什么样的策略去删除或者插入,它们是内部调用下面的两个私有函数 //而这两个私有函数只是是完成插入和删除的功能 };
COrderArray.cpp文件的全部代码如下:
#include<iostream> #include "COrderArray.h" COrderArray::COrderArray(int size) { m_Capacity = size; mp_Array = new int[m_Capacity]; } //在数组中要插入的元素位置之后的所有元素进行后移 void COrderArray::_InsertElement(int value, int pos) { for (int i = m_Size; i > pos; i--) { mp_Array[i] = mp_Array[i - 1]; } mp_Array[pos] = value; } //插入元素,判断元素大小来决定其应该在数组中位置 void COrderArray::PushBack(int value) { if (m_Size == m_Capacity) { return; } //二分查找 int start = 0; int end = m_Size; int mid = (start + end) / 2; while (1) { //数组还没有元素的时候,直接插入 if (m_Size == 0) { mp_Array[m_Size] = value; break; } //循环跳出的条件,即mid与首尾之一相等 if (mid==start||mid==end) { if (mp_Array[mid] > value) { _InsertElement(value, mid); break; } _InsertElement(value, mid + 1); break; } //大小判别,然后改变mid的值进行二分 if (mp_Array[mid] > value) { end = mid; mid = (start + end) / 2; } else { start = mid; mid = (start + end) / 2; } } m_Size++; } //根据值查找,二分查找,和上面的插入类似 int COrderArray::FindForValue (int value) { int start = 0; int end = m_Size; int mid = (start + end) / 2; while (mid != start && mid != end) { if (mp_Array[mid] == value) { return mid; } if (mp_Array[mid] > value) { end = mid; mid = (start + end) / 2; } else { start = mid; mid = (start + end) / 2; } } if (mp_Array[mid] == value) { return mid; } } //删除操作,把删除的数后面每一个元素向前移动一个位置 void COrderArray::_RemoveElement(int pos) { if (pos >= 0 && pos < m_Size) { for (int i = pos; i < m_Size; i++) { mp_Array[i] = mp_Array[i + 1]; } m_Size--; } } //根据指定位置删除元素 void COrderArray::RemoveForPos(int pos) { _RemoveElement(pos); } //根据指定值删除元素 void COrderArray::RemoveForValue(int value) { int pos = FindForValue(value); _RemoveElement(pos); } //重载下标运算符 int &COrderArray::operator[](int pos) { if (pos > m_Size || pos < 0) { return mp_Array[0]; } return mp_Array[pos]; } //清空所有元素 void COrderArray::Clear() { m_Size = 0; } //合并两个有序数组 void COrderArray::Merge(COrderArray& tempArray) { //开一个新的内存来存储合并后的数组,首先需要计算新的内存需要多大 int tempArrayCapacity = tempArray.getCapacity(); int newCapacity = tempArrayCapacity + m_Capacity; int* newSpace = new int[newCapacity]; //从两个数组当前size的位置开始合并 int tempArraySize = tempArray.getSize(); int tempSize = tempArraySize + m_Size; int pos = tempSize - 1; while (pos >= 0) { if (m_Size != 0) { if (mp_Array[m_Size - 1] >= tempArray[tempArraySize - 1]|| tempArraySize==0) { newSpace[pos] = mp_Array[m_Size - 1]; m_Size--; } else { newSpace[pos] = tempArray[tempArraySize - 1]; tempArraySize--; } }else { newSpace[pos] = tempArray[tempArraySize - 1]; tempArraySize--; } pos--; } delete[] mp_Array; m_Size = tempSize; m_Capacity = newCapacity; mp_Array = newSpace; }
Array.cpp文件的全部代码如下:
#include<iostream> using namespace std; #include "COrderArray.h" using namespace std; int main() { int n; cin >> n; //COrderArray* oArray = new COrderArray(n); COrderArray oArray(n); COrderArray oArray2(n); //插入第一个数组的数据 for (int i = 0; i < n; i++) { int temp; cin >> temp; oArray.PushBack(temp); } //插入第二个数组的数据 for (int i = 0; i < n; i++) { int temp; cin >> temp; oArray2.PushBack(temp); } oArray.Print();//打印数组 oArray.Merge(oArray2);//合并两个数组 oArray.Print();//重新打印 return 0; }