本文最后更新于 904 天前,其中的信息可能已经有所发展或是发生改变。
数组:
C、C++和Java等程序设计语言,都将数组作为一种内置的数据类型,支持对一组相关元素的存储组织与访问操作。
具体地,若集合 S
由 n
个元素组成,且各元素之间具有一个线性次序,可将它们存放于起始于地址 A
、物理位置连续的一段存储空间,并统称作数组(array)。
通常以 A
作为该数组的标识。具体地,数组 A[]
中的每一元素都唯一对应于某一下标编号,其中,对于任何 0 < i < j < n , A[i]
都是 A[j]
的前驱(predecessor),A[j]
都是 A[i]
的后继(successor)。
特别地,对于任何 i > 1 , A[i - 1]
称作 A[i]
的直接前驱(immediate predecessor),对于任何 i <= n - 2 , A[i + 1]
称作 A[i]
的直接后继(immediate successor)。
任一元素的所有前驱构成其前缀(prefix),所有后继构成其后缀(suffix)。
采用这一编号规范,不仅可以使得每个元素都通过下标唯一指代,而且可以使我们直接访问到任一元素。这里所说的"访问"包含读取、修改等基本操作,而"直接"则是指这些操作都可以在常数时间内完成。只要从数组所在空间的起始地址 A
出发,即可根据每一元素的编号,经过计算获得待访问元素的物理地址。
具体地,若数组 A[]
存放空间的起始地址为 A
,且每个元素占用 s
个单位的空间,则元素 A[i]
对应的物理地址为 A + i * s
,因其中元素的物理地址与其下标之间满足这种线性关系,故亦称作线性数组(linear array)。
向量:
按照面向对象思想中的数据抽象原则,可对以上的数组结构做一般性推广,使得其以上特性更具普遍性。
向量(vector)就是线性数组的一种抽象与泛化,它也是由具有线性次序的一组元素构成的集合。其中的元素分别由秩(rank)相互区分。例如:元素 e
的前驱元素共计 r
个, 则其秩就是 r
。
各元素的秩互异,且均为 [0,n)
内的整数。即,我们通过 r
可唯一确定 e
。这是向量特有的元素访问方式,,作"循秩访问"(call-by-rank)。
经如此抽象之后,我们不再限定同一向量中的各元素都属于同一基本类型,它们本身可以是来自于更具一般性的某一类的对象。

| #include <iostream> |
| #include <cstdlib> |
| using namespace std; |
| |
| typedef int Rank; |
| #define DEFAULT_CAPACITY 3 |
| |
| template <typename T> class Vector{ |
| protected: |
| |
| Rank _size; |
| int _capacity; |
| T *_elem; |
| |
| |
| void copyFrom(T const *A, Rank lo, Rank hi); |
| void expand(); |
| void shrink(); |
| |
| public: |
| |
| Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0){ |
| _elem = new T[_capacity = c]; |
| for (_size = 0; _size < s; _elem[_size ++] = v); |
| } |
| |
| Vector(T const *A, Rank n) { copyFrom(A, 0, n); } |
| Vector(T const *A, Rank lo, Rank hi) { copyFrom(A, lo, hi); } |
| Vector(Vector<T> const &V) { copyFrom(V._elem, 0, V._size); } |
| Vector(Vector<T> const &V, Rank lo, Rank hi) { copyFrom(V._elem, lo, hi); } |
| |
| |
| ~Vector() { delete[] _elem; } |
| |
| |
| |
| |
| T get(Rank r); |
| int capacity() const{ return _capacity; } |
| Rank size() const { return _size; } |
| bool empty() const { return !_size; } |
| Rank find(T const &e, Rank lo, Rank hi) const; |
| Rank find(T const &e) const { return find(e, 0, _size); } |
| Rank search(T const &e, Rank lo, Rank hi) const; |
| Rank search(T const &e) const { return (_size <= 0) ? -1 : search(e, 0, _size); } |
| |
| |
| T &operator[](Rank r) const; |
| Vector<T> &operator=(Vector<T> const &); |
| void put(Rank r, T const &e); |
| void unsort() { unsort(0, _size); } |
| void unsort(Rank lo, Rank hi); |
| void reverse() { reverse(0, _size); } |
| void reverse(Rank lo, Rank hi); |
| Rank insert(Rank r, T const &e); |
| Rank insert(T const &e) { return insert(_size, e); } |
| int remove(Rank lo, Rank hi); |
| T remove(Rank r); |
| int deduplicate(); |
| int uniquify(); |
| |
| |
| void traverse(void (*)(T &)); |
| template <typename VST> |
| void traverse(VST &); |
| |
| |
| bool bubble(Rank lo, Rank hi); |
| void bubbleSort(Rank lo, Rank hi); |
| void mergeSort(Rank lo,Rank hi); |
| void merge(Rank lo, Rank mi, Rank hi); |
| |
| }; |
由于向量的特性,我们选择数组这一结构作为向量类的基本元素单元,因为数组在内存中的物理地址与其逻辑次序一致。
向量在内部维护一个元素为 T
的私有数组 _elem[]
:其容量由私有变量 _capacity
指示,有效元素数量由 _size
指示,此外进一步约定:
- 向量中秩为
r
的元素,对应内部数组中的 _elem[r]
,其物理地址为 _elem + r
。
因此,向量对象的构造与析构将围绕这些私有变量和数据区的初始化与销毁展开。
与所有对象一样,向量在使用前也需首先被系统创建。
对于构造,我们重载了多个构造函数,其中默认构造方法是:
- 首先根据创建时的初始容量向系统申请空间,以创建私有数组
_elem[];
。
- 若容量未确定,则使用默认值
DEFAULT_CAPACITY
。
- 最后由于向量不包含任何元素,故指示规模的变量
_size
初始化为 $0$。
整个过程没有任何迭代,忽略用于分配数组空间的时间,共需常数时间。
| |
| Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0){ |
| _elem = new T[_capacity = c]; |
| for (_size = 0; _size < s; _elem[_size ++] = v); |
| } |
注意:
- 将默认构造函数在类声明中内联实现,创建向量时默认调用该函数。
向量的另一种典型创建方式,是以某已有的向量或数组为蓝本,进行(局部或整体)的克隆。
我们对于复制构造进行重载了多个接口,只要接口合法,就可以调用复制构造的核心方法 copyFrom()
。
| |
| template <typename T> |
| void Vector<T>::copyFrom(T const *A, Rank lo, Rank hi){ |
| _elem = new T[_capacity = 2 * (hi - lo)]; |
| _size = 0; |
| while (lo < hi){ |
| _elem[_size ++] = A[lo ++]; |
| } |
| } |
可提供的合法接口:
| Vector(T const *A, Rank n) { copyFrom(A, 0, n); } |
| Vector(T const *A, Rank lo, Rank hi) { copyFrom(A, lo, hi); } |
| Vector(Vector<T> const &V) { copyFrom(V._elem, 0, V._size); } |
| Vector(Vector<T> const &V, Rank lo, Rank hi) { copyFrom(V._elem, lo, hi); } |
解释:
copyFrom()
首先根据待复制区间的边界,换算出新向量的初始规模。
- 再以双倍的容量为内部数组
_elem[]
申请空间。
- 最后通过一次选代,完成区间
A[1o,hi)
内各元素的顺次复制。
若忽略开辟新空间所需的时间,运行时间应正比于区间宽度,即 $\mathcal{O}(hi - lo) = \mathcal{O}(\_size)$。
注意:
- 由于向量内部含有动态分配的空间默认的运算符
=
不足以支持向量之间的直接赋值。故通过默认赋值运算符并不能复制向量内部的数据区。
为适应此类赋值操作的需求,我们重载 =
和 []
操作符:
| |
| template <typename T> |
| Vector<T> &Vector<T>::operator=(const Vector<T> &V){ |
| delete[] _elem; |
| copyFrom(V._elem, 0, V._size); |
| return *this; |
| } |
| |
| template <typename T> |
| T &Vector<T>::operator[](Rank r) const{ |
| return _elem[r]; |
| } |
与所有对象一样,不再需要的向量应借助析构函数(destructor)及时清理(clean up), 以释放其占用的系统资源。与构造函数不同,同一对象只能有一个析构函数,且不得重载。
向量对象的析构,只需释放用于存放元素的内部数组 _elem[]
,将其占用的空间交还操作系统。_capacity
和 _size
之类的内部变量无需做任何处理,它们将作为向量对象自身的一部分被系统回收,此后既无需也无法被引用。
| |
| ~Vector() { delete[] _elem; } |
若不计系统用于空间回收的时间,整个析构过程只需 $\mathcal{O}(1)$ 的时间。
每次插入元素时,我们要检查向量空间大小,若空间不足以插入新的元素,则要扩充向量(extendable vector)。
若要动态实现扩容,我们不能直接在原有的物理空间基础上追加空间。因为数组特有的定址方式要求,物理空间必须地址连续,而我们无法保证,当前向量尾部预留了足够的空间可供扩展。
一种可行的方法如下,我们可以申请一个容量更大的数组 B[]
,并且将原数组 A[]
中的成员集体搬迁至新的空间,再删除原来的数组 A[]
。此后即可顺利地在 B[]
中插入的新元素 e
从而不会导致上溢(overflow)。
那么我们要申请的新容量多少才合适?
有以下两种策略:
- 容量递增策略:每次扩容,追加固定增量。
- 容量加倍策略:每次扩容,容量加倍。
假设我们每次开辟新空间时,比原有的空间增加固定的大小 INCREMENT
。
T* oldElem = _elem; _elem = new T[ _capacity += INCREMENT ] //容量递增;
我们考虑最坏的扩容情况:
- 在初始容量为 $0$ 的空向量中,连续插入 $n = m\times I \gg 2$个元素,且无删除操作。
则:
- 我们在第 $I,I +1,2I+1,3I+1\dots,(m-1)I+1$ 次插入时,都需扩容。
即便不计申请空间操作,各次扩容过程中复制原向量的时间成本依次为:
- $0,I,2I,3I,\dots,(m-1)I$,算数级数。
时间复杂度:$\mathcal{O}(n^2)$,每次(insert/remove)操作的分摊成本为 $\mathcal{O}(n)$。
假设我们每次开辟新空间时,增加原有空间一倍的大小。
T* oldElem = _elem; _elem = new T[ _capacity <<= 1 ]; //容量加倍
我们考虑最坏的扩容情况:
- 在初始容量为 $0$ 的空向量中,连续插入 $n = 2^m \gg 2$ 个元素,且无删除操作。
则:
- 我们在第 $1,2,4,8,\dots,2^{m-1}$ 次插入时,都需要扩容。
不计申请空间操作,各次扩容过程中复制原向量的时间成本依次为:
- $1,2,4,8,\dots,2^{m-1},2^m = n$ ,几何级数。
时间复杂度:$\mathcal{O}(n)$,每次(insert/remove)操作的分摊成本为 $\mathcal{O}(1)$。
由动态扩容对比可知,显然容量加倍的动态扩容策略更好,因此我们选择该策略实现向量扩容:
| |
| template <typename T> |
| void Vector<T>::expand(){ |
| while (_size == _capacity){ |
| T *oldElem = _elem; |
| _elem = new T[_capacity <<= 1]; |
| for (int i = 0; i < _size; i++){ |
| _elem[i] = oldElem[i]; |
| } |
| delete[] oldElem; |
| } |
| } |
上述的扩容策略能够很好的保证我们的向量空间不足时,以较高的效率进行扩容。
导致低效率的另一情况是,向量的实际规模可能远远小于内部数组的容量。比如在连续的一系列操作过程中,若删除操作远多于插入操作,则装填因子极有可能远远小于 $100\%$,甚至非常接近于 $0$。当装填因子低于某一阀值时,我们称数组发生了下溢(underflow)。
尽管下溢不属于必须解决的问题,但在格外关注空间利用率的场合发生下溢时,我们有必要适当缩减内部数组容量。
接下来提供一种可行的缩容算法 shrink()
:
| |
| template <typename T> |
| void Vector<T>::shrink(){ |
| while (_size << 2 < _capacity){ |
| T *oldElem = _elem; |
| _elem = new T[_capacity >>= 1]; |
| for (int i = 0; i < _size; i ++){ |
| _elem[i] = oldElem[i]; |
| } |
| delete[] oldElem; |
| } |
| } |
每次删除操作之后,一旦空间利用率已降至某一阈值以下,该算法随即申请一个容量减半的新数组,将原数组中的元素逐一搬迁至其中,最后将原数组所占空间交还操作系统。
这里以 $25\%$ 作为装填因子的下限,实际应用中,为避免频繁缩容,可使用更低阀值,取 $0$ 时即为禁止缩容。
与 expand()
操作类似,尽管单次 shrink()
操作需要线性量级的时间,但其分摊复杂度亦为 $\mathcal{O}(1)$。实际上 shrink()
过程等效于 expand()
的逆过程。
这两个算法相互配合,在不致实质地增加接口操作复杂度的前提下,保证了向量内部空间的高效利用。在对单次操作的执行速度极其敏感的应用场合,以上策略并不适用,其中缩容操作甚至可以完全不予考虑。
从待置乱区间的末元素开始,逆序地向前逐一处理各元素。对每一个当前元素 V[i - 1]
,先通过调用 rand()
函数在[0,i)
之间等概率地随机选取一个元素,再令二者互换位置。
注意:
- 使用
rand()
需导入头文件 <cstdlib>
。
- 这里的交换操作
swap()
,隐含了三次基于重载操作符 []
的赋值。
于是每经过一步这样的选代,置乱区间都会向前拓展一个单元。因此经过 $\mathcal{O}(n)$ 步选代之后,即实现了整个向量的置乱。
| |
| template <typename T> |
| void permute(Vector<T>& V){ |
| for(int i = V.size(); i > 0; i --){ |
| swap(V[i - 1],V[rand() % i]); |
| } |
| } |
解释:
- 理论上来说,该算法不仅可以枚举出同一向量的所有可能的排列,且可以保证生成各种排列的概率相等。
我们不妨将其封装至 ADT
中:
| |
| template <typename T> |
| void Vector<T>::unsort(Rank lo, Rank hi){ |
| T *V = _elem + lo; |
| for (Rank i = hi - lo; i > 0; i --){ |
| std::swap(V[i - 1], V[rand() % i]); |
| } |
| } |
通过该接口,可以均匀地置乱区间 [lo,hi]
内的元素。
同理,我们也可以封装一个就地逆置方法:
| |
| template <typename T> |
| void Vector<T>::reverse(Rank lo, Rank hi){ |
| T *V = _elem; |
| Rank l = lo, r = hi - 1; |
| while(l < r){ |
| std::swap(V[l],V[r]); |
| l ++, r--; |
| } |
| } |
该方法可以实现对区间 [lo,hi)
的元素逆序,且不占用额外的空间。
对于无序的向量,查找任意指定元素 e
时,由于没有更多的信息可以借助。故在最坏的情况下,对所有元素进行遍历,直到找到该元素。这里针对无序向量的整体或区间查找重载了两个 find()
接口,整体查找作为特例可直接调用区间查找来完成。因此,我们只需实现区间查找的接口:
| |
| template <typename T> |
| Rank Vector<T>::find(T const &e, Rank lo, Rank hi) const{ |
| while ((lo < hi --) && (e != _elem[hi])); |
| return hi; |
| } |
解释:
- 当同时有多个命中元素时,我们统一约定返回其中秩最大者, 之后的查找接口
find()
亦是如此。
- 采用自后向前的查找次序,一旦命中即可立即返回,从而省略掉不必要的比对。
- 查找失败时约定统一返回 $-1$。这不仅简化了对查找失败情况的判别,同时也使此时的返回结果更加易于理解。
while
循环的控制逻辑由两部分组成,首先判断是否已抵达通配符,再判断当前元素与目标元素是否相等。得益于C/C++语言中逻辑表达式的短路求值特性,在前一判断非真后循环会立即终止,而不致可能因试图引用已越界的秩 ($-1$)而出错。
最坏情况下,查找终止于首元素 _elem[1o]
,运行时间为 $\mathcal{O}(hi-lo) = \mathcal{O}(n)$。最好情况下,查找命中于末元素 _elem[hi - 1]
,仅需 $\mathcal{O}(1)$ 时间。对于规模相同、内部组成不同的输入,渐进运行时间却有本质区别。故此类算法也称作输入敏感的(input sensitive)算法。
对于一个有序向量 S
,其中的元素不再随机分布,秩 r
是 S[r]
在 S
中按大小的相对位次,位于 S[r]
前(后)方的元素均不致于更大(小)。当所有元素互异时,r
即是 S
中小于 S[r]
的元素数目。一般地,,若小于、等于 S[r]
的元素各有i
、k
个,则该元素及其雷同元素应集中分布于 S[i, i + k)
。
利用上述性质,有序向量的查找操作可以利用二分查找高效地完成。为区别于无序向量的查找接口 find()
,有序向量的查找接口将统一命名为 search()
。与 find()
一样,也针对有序向量的整体或区间查找重载了两个 search()
接口,且前者作为特例可直接调用后者。
| |
| template <typename T> |
| Rank Vector<T>::search(T const &e, Rank lo, Rank hi) const{ |
| T* A = _elem; |
| while ( lo < hi ){ |
| Rank mi = ( lo + hi ) >> 1; |
| ( e < A[mi] ) ? hi = mi : lo = mi + 1; |
| } |
| Rank p = -- lo; |
| if(A[p] == e) return p; |
| return -1; |
| } |
解释:
- 只有当有效区间的宽度缩短至 $0$ 时,查询结束。
- 在每次转入后端分支时,由于子向量的左边界取作
mi + 1
而不是 mi
,通过数学归纳可以证明,循环体内具有如下不变性:A[0,lo)
中的元素皆不大于 e
;A[hi,n)
中的元素皆大于 e
。故不会忽略 A[mi]
。
- 循环终止时,
lo = hi
,即 A[1o - 1]
为原向量中不大于 e
的最后一个元素。因此在循环结束之后,无论成功与否,只需返回lo - 1
。
- 有多个命中元素时,总能保证返回秩最大者;查找失败时,能够返回 $-1$。
综上,整个算法时间复杂度仍保持在 $\mathcal{O}(\log_2^n)$ 。
按照 ADT
的定义,提供 get(r)
,获取向量秩为 r
的元素的值;提供 put(r, e)
,修改向量秩为 r
的元素为 e
。
| |
| template <typename T> |
| T Vector<T>::get(Rank r){ |
| T value = _elem[r]; |
| return value; |
| } |
| |
| template <typename T> |
| void Vector<T>::put(Rank r, T const &e){ |
| _elem[r] = e; |
| } |
按照 ADT
的定义,插入操作 insert(r,e)
负责将任意给定的元素 e
插入到任意指定秩为 r
的单元。我们同样重载接口,来同时实现尾部插入,故作为区间插入的特例,我们只需实现指定秩的元素插入:
| |
| template <typename T> |
| Rank Vector<T>::insert(Rank r, T const &e){ |
| expand(); |
| for (int i = _size; i > r;) _elem[i] = _elem[i - 1]; |
| _elem[r] = e, _size ++; |
| return r; |
| } |
解释:
- 插入单个元素,届时需要检查容量是否支持该次插入操作,不支持时需要扩容。
该操作所需时间主要在于后继元素的后移,线性正比于后缀的长度。新插入元素越靠后(前)所需时间越短(长)。当 r
取最大值 _size
时为最好情况,只需 $\mathcal{O}(1)$ 时间,r
取最小值 $0$ 时为最坏情况,需要 $\mathcal{O}(\_size)$ 时间。
一般地,若插入位置等概率分布,则平均运行时间为 $\mathcal{O}(n)$,线性正比于向量的实际规模。
删除操作重载有两个接口,remove(lo,hi)
用以删除区间 [lo,hi)
内的元素,而 remove(r)
用以删除秩为 r
的单个元素。因数组中元素的地址必须连续,故每删除一个元素时,所有后继元素都需向前移动一个单元。若后继元素共有 m = _size - hi
个,则对 remove(r)
的每次调用都需移动 m
次,对于整个区间,元素移动的次数累计将达到 m*(hi - lo)
,为后缀长度和待删除区间宽度的乘积。故对于这两个接口,我们应将单元素删除视作区间删除的特例,并基于后者来实现前者:
| |
| template <typename T> |
| int Vector<T>::remove(Rank lo, Rank hi){ |
| if (lo == hi) return 0; |
| while(hi < _size){ |
| _elem[lo ++] = _elem[hi ++]; |
| } |
| _size = lo; |
| if(this -> size() > 0) shrink(); |
| return hi - lo; |
| } |
| |
| template <typename T> |
| T Vector<T>::remove(Rank r){ |
| T re_elem = _elem[r]; |
| remove(r, r + 1); |
| return re_elem; |
| } |
解释:
- 区间删除元素,删除后,检查当前的容量是否过大,当若实际规模不到容量的25%,则缩容。
remove(lo, hi)
的计算成本,主要消耗于后续元素的前移,线性正比于后缀的长度。区间删除操作所需的时间,应该仅取决于后继元素的数目,而与被删除区间本身的宽度无关。
特别地,基于该接口实现的单元素删除接口 remove(r)
,被删除元素在向量中的位置越靠后 (前)所需时间越短(长),最好为删除末尾元素,只需 $\mathcal{O}(1)$ 时间,最坏情况下删除首元素,需要 $\mathcal{O}(\_size)$ 时间。
若想对无序向量进行去重操作,我们只需在当前元素的前缀中寻找相同的元素。如找到,则删除该元素,如没有找到,则转到该元素的后继,继续重复上述操作。
| |
| template <typename T> |
| int Vector<T>::deduplicate(){ |
| int oldSize = _size; |
| Rank i = 1; |
| while (i < _size){ |
| (find(_elem[i], 0, i) < 0) ? |
| i ++ : remove(i); |
| } |
| return oldSize - _size; |
| } |
解释:
- 在循环体内,有如下不变性:在当前元素的前缀
_elem[0,i)
内,不存在重复元素。
- 随着循环进行,当前元素的后继不断减少,经过
n - 2
步迭代后结束。
这里所需时间主要消耗于 find()
和 remove()
两个接口。每次迭代的时间为 $\mathcal{O}(n)$,总体的时间复杂度为 $\mathcal{O}(n^2)$。
将无序向量唯一化,我们通常会将其转化为有序向量,然后对其有序向量进行去重操作。
对于有序向量,重复的元素必然是连续的区间,因此我们可以对重复的元素进行区间删除,从而实现有序向量的去重操作。
| |
| template <typename T> |
| int Vector<T>::uniquify(){ |
| int i = 0, j = 0; |
| while(++ j < _size){ |
| if(_elem[i] != _elem[j]){ |
| _elem[++i ] = _elem[j]; |
| } |
| } |
| _size = ++ i; shrink(); |
| return j - i; |
| } |
整个算法中,每经过一次迭代, j
都必然加一,故最多迭代 n
次,最终算法的时间复杂度为 $\mathcal{O}(n)$,比无序去重效率提高了一个线性因子。
向量往往作为整体进行统一操作,如输出向量的所有元素,或按照某种流程统一修改所有元素值。针对这些批量操作,我们都为其提供接口。
遍历向量,对每个元素执行函数指针提供的操作:
| |
| template <typename T> |
| void Vector<T>::traverse(void (*visit)(T&)){ |
| for (int i = 0; i < _size; i ++){ |
| visit(_elem[i]); |
| } |
| } |
遍历向量,对每个元素执行函数对象(一种重载()
操作符的特殊类,其实例对象可以像函数一样调用)提供的操作:
| |
| template <typename T> |
| template <typename VST> |
| void Vector<T>::traverse(VST& visit){ |
| for (int i = 0; i < _size; i ++){ |
| visit(_elem[i]); |
| } |
| } |
我们可以自定义函数指针和函数对象来执行批量的任务。
实例:
实现遍历输出:
| template <typename T, typename VST> |
| void traverse(VST& visit, T& V, Rank lo, Rank hi) { |
| for(Rank i = lo; i <= hi; i ++){ |
| visit(V[i]); |
| } |
| } |
| |
| |
| void show(int e) { |
| cout << e << " "; |
| } |
调用:
- 将无序向量变为有序向量,我们需要借助排序算法。
- 本节提供一种稳定的排序方法——冒泡排序,以及高效实用的归并排序算法。
排序的稳定性:
- 稳定性(stability)是对排序算法更为细致的要求。
- 具体地,在将向量
A
转换为有序向量 S
之后,设 A[i]
对应于 S[ki]
。若对于 A
中每一对重复元素 A[i] = A[j]
(相应地 S[ki] = S[kj]
),都有 i < j
当且仅当 ki< kj
则称该排序算法是稳定算法 (stable algorithm)。
- 简而言之,稳定算法的特征是,重复元素之间的相对次序在排序前后保持一致。反之,不具有这一特征的排序算法都是不稳定算法(unstable algorithm)。
- 稳定的排序算法,可用以实现同时对多个关键码按照字典序的排序。
提供接口 bubbleSort()
封装在 ADT
中,调用时对向量区间 [lo,hi)
进行冒泡排序。
| |
| template <typename T> |
| void Vector<T>::bubbleSort(Rank lo, Rank hi) |
| { while (!bubble(lo,hi --)); } |
| |
| |
| template <typename T> |
| bool Vector<T>::bubble(Rank lo, Rank hi){ |
| bool sorted = true; |
| while (++ lo < hi){ |
| if (_elem[lo - 1] > _elem[lo]){ |
| sorted = false; |
| std::swap(_elem[lo - 1], _elem[lo]); |
| } |
| } |
| return sorted; |
| } |
解释:
- 冒泡排序过程中元素相对位置有所调整的唯一可能是:某元素
_elem[i - 1]
严格大于其后继 _elem[i]
,故属于稳定算法。
该算法最好的情况在向量乱序限于 [0,sqrt(n)]
时,仍需 $\mathcal{O}(n^{\frac{3}{2}})$,否则需要 $\mathcal{O}(n^2)$ 的时间。
归并排序(merge sort)是第一个可以在最坏情况下依然保持 $\mathcal{O}(n\log_2^n)$ 运行时间的确定性排序算法。
归并排序基于分治策略:
- 先取需要排序的向量中间位置,将其划分为左右两个子区间。 递归重复上述处理操作,直至区间无法继续划分。
- 递归返回时,分别对左右两个区间进行排序,并反复调用二路归并算法,将相邻等长的子区间不断合并成更大的有序区间,直到最终得到整个有序向量。
接下来我们分别提供递归处理区间的接口和二路归并的接口,以适应不同需求:
| |
| template <typename T> |
| void Vector<T>::mergeSort(Rank lo, Rank hi){ |
| if(hi - lo < 2) return; |
| int mi = (lo + hi) >> 1; |
| mergeSort(lo, mi); |
| mergeSort(mi, hi); |
| merge(lo, mi, hi); |
| } |
| |
| template <typename T> |
| void Vector<T>::merge(Rank lo, Rank mi, Rank hi){ |
| T* A = _elem + lo; |
| int lb = mi - lo; T* B = new T[lb]; |
| for(Rank i = 0; i < lb; i ++) B[i] = A[i]; |
| int lc = hi - mi; T* C = _elem + mi; |
| for(Rank i = 0, j = 0, k = 0; (j < lb) || (k < lc);){ |
| if((j < lb) && (!(k < lc) || (B[j] <= C[k]))) A[i ++] = B[j ++]; |
| if((k < lc) && (!(j < lb) || (C[k] < B[j]))) A[i ++] = C[k ++]; |
| } |
| delete [] B; |
| } |
| #include <iostream> |
| #include <cstdlib> |
| using namespace std; |
| |
| typedef int Rank; |
| #define DEFAULT_CAPACITY 3 |
| |
| template <typename T> class Vector{ |
| protected: |
| |
| Rank _size; |
| int _capacity; |
| T *_elem; |
| |
| |
| void copyFrom(T const *A, Rank lo, Rank hi); |
| void expand(); |
| void shrink(); |
| |
| public: |
| |
| Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0){ |
| _elem = new T[_capacity = c]; |
| for (_size = 0; _size < s; _elem[_size ++] = v); |
| } |
| |
| Vector(T const *A, Rank n) { copyFrom(A, 0, n); } |
| Vector(T const *A, Rank lo, Rank hi) { copyFrom(A, lo, hi); } |
| Vector(Vector<T> const &V) { copyFrom(V._elem, 0, V._size); } |
| Vector(Vector<T> const &V, Rank lo, Rank hi) { copyFrom(V._elem, lo, hi); } |
| |
| |
| ~Vector() { delete[] _elem; } |
| |
| |
| |
| |
| T get(Rank r); |
| int capacity() const{ return _capacity; } |
| Rank size() const { return _size; } |
| bool empty() const { return !_size; } |
| Rank find(T const &e, Rank lo, Rank hi) const; |
| Rank find(T const &e) const { return find(e, 0, _size); } |
| Rank search(T const &e, Rank lo, Rank hi) const; |
| Rank search(T const &e) const { return (_size <= 0) ? -1 : search(e, 0, _size); } |
| |
| |
| T &operator[](Rank r) const; |
| Vector<T> &operator=(Vector<T> const &); |
| void put(Rank r, T const &e); |
| void unsort() { unsort(0, _size); } |
| void unsort(Rank lo, Rank hi); |
| void reverse() { reverse(0, _size); } |
| void reverse(Rank lo, Rank hi); |
| Rank insert(Rank r, T const &e); |
| Rank insert(T const &e) { return insert(_size, e); } |
| int remove(Rank lo, Rank hi); |
| T remove(Rank r); |
| int deduplicate(); |
| int uniquify(); |
| |
| |
| void traverse(void (*)(T &)); |
| template <typename VST> |
| void traverse(VST &); |
| |
| |
| bool bubble(Rank lo, Rank hi); |
| void bubbleSort(Rank lo, Rank hi); |
| void mergeSort(Rank lo,Rank hi); |
| void merge(Rank lo, Rank mi, Rank hi); |
| |
| }; |
| |
| |
| template <typename T> |
| void Vector<T>::copyFrom(T const *A, Rank lo, Rank hi){ |
| _elem = new T[_capacity = 2 * (hi - lo)]; |
| _size = 0; |
| while (lo < hi){ |
| _elem[_size ++] = A[lo ++]; |
| } |
| } |
| |
| |
| template <typename T> |
| Vector<T> &Vector<T>::operator=(const Vector<T> &V) |
| { |
| delete[] _elem; |
| copyFrom(V._elem, 0, V._size); |
| return *this; |
| } |
| |
| |
| template <typename T> |
| T &Vector<T>::operator[](Rank r) const{ |
| return _elem[r]; |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::expand(){ |
| while (_size == _capacity){ |
| T *oldElem = _elem; |
| _elem = new T[_capacity <<= 1]; |
| for (int i = 0; i < _size; i++){ |
| _elem[i] = oldElem[i]; |
| } |
| delete[] oldElem; |
| } |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::shrink(){ |
| while (_size << 2 < _capacity){ |
| T *oldElem = _elem; |
| _elem = new T[_capacity >>= 1]; |
| for (int i = 0; i < _size; i ++){ |
| _elem[i] = oldElem[i]; |
| } |
| delete[] oldElem; |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| template <typename T> |
| void Vector<T>::unsort(Rank lo, Rank hi){ |
| T *V = _elem + lo; |
| for (Rank i = hi - lo; i > 0; i --){ |
| std::swap(V[i - 1], V[rand() % i]); |
| } |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::reverse(Rank lo, Rank hi){ |
| T *V = _elem; |
| Rank l = lo, r = hi - 1; |
| while(l < r){ |
| std::swap(V[l],V[r]); |
| l ++, r--; |
| } |
| } |
| |
| |
| template <typename T> |
| Rank Vector<T>::find(T const &e, Rank lo, Rank hi) const{ |
| while ((lo < hi --) && (e != _elem[hi])); |
| return hi; |
| } |
| |
| |
| template <typename T> |
| Rank Vector<T>::search(T const &e, Rank lo, Rank hi) const{ |
| T* A = _elem; |
| while ( lo < hi ){ |
| Rank mi = ( lo + hi ) >> 1; |
| ( e < A[mi] ) ? hi = mi : lo = mi + 1; |
| } |
| Rank p = -- lo; |
| if(A[p] == e) return p; |
| return -1; |
| } |
| |
| |
| template <typename T> |
| T Vector<T>::get(Rank r){ |
| T value = _elem[r]; |
| return value; |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::put(Rank r, T const &e){ |
| _elem[r] = e; |
| } |
| |
| |
| template <typename T> |
| Rank Vector<T>::insert(Rank r, T const &e){ |
| expand(); |
| for (int i = _size; i > r;) _elem[i] = _elem[i - 1]; |
| _elem[r] = e, _size ++; |
| return r; |
| } |
| |
| |
| template <typename T> |
| int Vector<T>::remove(Rank lo, Rank hi){ |
| if (lo == hi) return 0; |
| while(hi < _size){ |
| _elem[lo ++] = _elem[hi ++]; |
| } |
| _size = lo; |
| if(this -> size() > 0) shrink(); |
| return hi - lo; |
| } |
| |
| |
| template <typename T> |
| T Vector<T>::remove(Rank r){ |
| T re_elem = _elem[r]; |
| remove(r, r + 1); |
| return re_elem; |
| } |
| |
| |
| template <typename T> |
| int Vector<T>::deduplicate(){ |
| int oldSize = _size; |
| Rank i = 1; |
| while (i < _size){ |
| (find(_elem[i], 0, i) < 0) ? |
| i ++ : remove(i); |
| } |
| return oldSize - _size; |
| } |
| |
| |
| template <typename T> |
| int Vector<T>::uniquify(){ |
| int i = 0, j = 0; |
| while(++ j < _size){ |
| if(_elem[i] != _elem[j]){ |
| _elem[++i ] = _elem[j]; |
| } |
| } |
| _size = ++ i; shrink(); |
| return j - i; |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::traverse(void (*visit)(T&)){ |
| for (int i = 0; i < _size; i ++){ |
| visit(_elem[i]); |
| } |
| } |
| |
| |
| template <typename T> |
| template <typename VST> |
| void Vector<T>::traverse(VST& visit){ |
| for (int i = 0; i < _size; i ++){ |
| visit(_elem[i]); |
| } |
| } |
| |
| template <typename T, typename VST> |
| void traverse(VST& visit, T& V, Rank lo, Rank hi) { |
| for(Rank i = lo; i <= hi; i ++){ |
| visit(V[i]); |
| } |
| } |
| |
| |
| void show(int e) { |
| cout << e << " "; |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::bubbleSort(Rank lo, Rank hi) |
| { while (!bubble(lo,hi --)); } |
| |
| |
| template <typename T> |
| bool Vector<T>::bubble(Rank lo, Rank hi){ |
| bool sorted = true; |
| while (++ lo < hi){ |
| if (_elem[lo - 1] > _elem[lo]){ |
| sorted = false; |
| std::swap(_elem[lo - 1], _elem[lo]); |
| } |
| } |
| return sorted; |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::mergeSort(Rank lo, Rank hi){ |
| if(hi - lo < 2) return; |
| int mi = (lo + hi) >> 1; |
| mergeSort(lo, mi); |
| mergeSort(mi, hi); |
| merge(lo, mi, hi); |
| } |
| |
| |
| template <typename T> |
| void Vector<T>::merge(Rank lo, Rank mi, Rank hi){ |
| T* A = _elem + lo; |
| int lb = mi - lo; T* B = new T[lb]; |
| for(Rank i = 0; i < lb; i ++) B[i] = A[i]; |
| int lc = hi - mi; T* C = _elem + mi; |
| for(Rank i = 0, j = 0, k = 0; (j < lb) || (k < lc);){ |
| if((j < lb) && (!(k < lc) || (B[j] <= C[k]))) A[i ++] = B[j ++]; |
| if((k < lc) && (!(j < lb) || (C[k] < B[j]))) A[i ++] = C[k ++]; |
| } |
| delete [] B; |
| } |
| |
| int num[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; |
| |
| void test_01(){ |
| |
| cout << "##### test_01() begin #####" << endl << endl; |
| |
| cout << "Test Vector Init :" << endl; |
| |
| Vector<int> a; |
| Vector<double> b; |
| Vector<Vector<int>> c; |
| |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "Test Vector copyFrom() :" << endl; |
| |
| Vector<int> d(num,1,10); |
| Vector<int> e = d; |
| Vector<int> f(e,0,2); |
| |
| cout << "d = "; |
| traverse(show, d, 0, 9); |
| cout << endl; |
| |
| cout << "e = "; |
| traverse(show, e, 0,9); |
| cout << endl; |
| |
| cout << "f = "; |
| traverse(show, f, 0,1); |
| cout << endl; |
| |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "##### test_01() over #####" << endl; |
| |
| } |
| |
| void test_02(){ |
| |
| cout << "##### test_02() begin #####" << endl << endl; |
| |
| cout << "Test Vector expend() :" << endl; |
| |
| Vector<int> a(num,0,2); |
| cout << "a.size() = " << a.size() << endl; |
| cout << "a.capacity() = " << a.capacity() << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| Vector<int> b(num,0,10); |
| cout << "b.size() = " << b.size() << endl; |
| cout << "b.capacity() = " << b.capacity() << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "##### test_02() over #####" << endl; |
| |
| } |
| |
| void test_03(){ |
| |
| cout << "##### test_03() begin #####" << endl << endl; |
| |
| cout << "Test Vector unsort() :" << endl; |
| |
| Vector<int> a(num,0,10); |
| |
| cout << "a = "; |
| traverse(show, a, 0, 9); |
| cout << endl; |
| |
| a.unsort(); |
| |
| cout << "unsort a = "; |
| traverse(show, a, 0, 9); |
| cout << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| Vector<int> b(num,0,10); |
| |
| cout << "b = "; |
| traverse(show, b, 0, 9); |
| cout << endl; |
| |
| b.unsort(2,9); |
| |
| cout << "unsor[2,9) b = "; |
| traverse(show, b, 0, 9); |
| cout << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "Test reverse() :" << endl; |
| |
| Vector<int> c(num,0,10); |
| |
| cout << "c = "; |
| traverse(show, c, 0, 9); |
| cout << endl; |
| |
| c.reverse(); |
| |
| cout << "reverse c = "; |
| traverse(show, c, 0, 9); |
| cout << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| Vector<int> d(num,0,10); |
| |
| cout << "d = "; |
| traverse(show, d, 0, 9); |
| cout << endl; |
| |
| d.reverse(1,5); |
| |
| cout << "reverse[1,5) d = "; |
| traverse(show, d, 0, 9); |
| cout << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "##### test_03() over #####" << endl; |
| |
| } |
| |
| void test_04(){ |
| |
| cout << "##### test_04() begin #####" << endl << endl; |
| |
| cout << "Test Vector find() :" << endl; |
| |
| Vector<int> a(num,0,10); |
| a.unsort(); |
| cout << "a = "; |
| traverse(show, a, 0, 9); |
| cout << endl; |
| |
| cout << "find(9) = " << a.find(9) << endl; |
| cout << "find(100) = " << a.find(100) << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "Test Vector search() :" << endl; |
| |
| Vector<int> b(num,0,10); |
| cout << "b = "; |
| traverse(show, b, 0, 9); |
| cout << endl; |
| |
| cout << "search(9) = " << b.search(9) << endl; |
| cout << "search(100) = " << b.search(100) << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "Test Vector put() :" << endl; |
| |
| Vector<int> c(num,0,10); |
| cout << "c = "; |
| traverse(show, c, 0, 9); |
| cout << endl; |
| |
| c.put(0,100); |
| c.put(2,-10); |
| c.put(9,20); |
| cout << "put() c = "; |
| traverse(show, c, 0, 9); |
| cout << endl; |
| |
| cout << "c[4] = " << c.get(4) << endl; |
| cout << "c[6] = " << c.get(6) << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "##### test_04() over #####" << endl; |
| |
| } |
| |
| void test_05(){ |
| |
| cout << "##### test_05() begin #####" << endl << endl; |
| |
| cout << "Test Vector insert() :" << endl; |
| |
| Vector<int> a; |
| |
| for(int i = 0; i < 5; i ++) a.insert(num[i]); |
| |
| cout << "a = "; |
| traverse(show, a, 0, 4); |
| cout << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "Test Vector deduplicate() :" << endl; |
| |
| Vector<int> b(num,0,10); |
| for(int i = 0; i < 5; i ++) b.insert(num[i]); |
| b.unsort(); |
| cout << "b = "; |
| traverse(show, b, 0, b.size() - 1); |
| cout << endl; |
| |
| b.deduplicate(); |
| |
| cout << "deduplicate b = "; |
| traverse(show, b, 0, b.size() - 1); |
| cout << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "Test Vector uniquify() :" << endl; |
| |
| Vector<int> c(num,0,10); |
| for(int i = 0; i < 5; i ++) c.insert(num[i]); |
| c.unsort(); |
| |
| c.mergeSort(0, c.size()); |
| |
| |
| cout << "c = "; |
| traverse(show, c, 0, c.size() - 1); |
| cout << endl; |
| |
| int s = c.uniquify(); |
| |
| cout << "c = "; |
| traverse(show, c, 0, c.size() - 1); |
| cout << endl; |
| cout << "RIGHT!" << endl << endl; |
| |
| cout << "##### test_05() over #####" << endl; |
| |
| } |
| |
| int main(){ |
| |
| cout << "###########################" << endl; |
| test_01(); |
| cout << "###########################" << endl; |
| cout << endl; |
| |
| cout << "###########################" << endl; |
| test_02(); |
| cout << "###########################" << endl; |
| cout << endl; |
| |
| cout << "###########################" << endl; |
| test_03(); |
| cout << "###########################" << endl; |
| cout << endl; |
| |
| cout << "###########################" << endl; |
| test_04(); |
| cout << "###########################" << endl; |
| cout << endl; |
| |
| cout << "###########################" << endl; |
| test_05(); |
| cout << "###########################" << endl; |
| cout << endl; |
| |
| system("pause"); |
| |
| return 0; |
| |
| } |