文章目录
一、循位置访问
1.从静态到动态
-
根据是否修改数据结构,所有操作大致分为两类方式
- 静态: 仅读取,数据结构的内容及组成一般不变:get、search
- 动态: 需写入,数据结构的局部或整体将改变:put、insert、remove
-
与操作方式相对应地,数据元素的存储与组织方式也分为两种
- 静态
- 数据空间整体创建或销毁
- 数据元素的物理存储次序与其逻辑次序严格一致;可支持高效的静态操作
- 比如向量,元素的物理地址与其逻辑次序线性对应
- 动态
- 为各数据元素动态地分配和回收的物理空间
- 相邻元素记录彼此的物理地址,在逻辑上形成一个整体;可支持高效的动态操作
- 静态
2.从向量到列表
-
列表(list)是采用动态储存策略的典型结构
- 其中的元素称作节点(node),通过指针或引用彼此连接
- 在逻辑上构成一个线性序列:L = { a0, a1, …, an-1 }
-
相邻节点彼此互称前驱(predecessor)或后继(successor)
- 没有前驱/后继的节点称作首(first/front)/末(last/rear)节点
3.从秩到位置
- 向量支持循秩访问(call-by-rank):根据元素的秩,可在O(1)时间内直接确定其物理地址
- 然而,此时的循秩访问成本过高,已不合时宜;因此,应改用循位置访问(call-by-position)的方式,亦即,转而利用节点之间的相互引用,找到特定的节点
二、接口与实现
1.列表节点:ADT接口
-
作为列表的基本元素 列表节点首先需要 独立地“封装”实现
-
基本操作接口
|操作接口 |功能 |
|–|–|
|pred() |当前节点前驱节点的位置 |
|succ() |当前节点后继节点的位置|
|data() |当前节点所存数据对象 |
|insertAsPred(e) |插入前驱节点,存入被引用对象e,返回新节点位置|
|insertAsSucc(e) |插入后继节点,存入被引用对象e,返回新节点位置| -
ListNode模板
template<typename T> using ListNodePosi = ListNode*; //列表节点位置(C++.0x)
template<typename T> struct ListNode { //简洁起见,完全开放而不再严格封装
T data;
ListNodePosi pred;
ListNodePosi succ;
ListNode() {} //针对header和trailer的构造
ListNode(T e, ListNodePosi p = NULL, ListNodePosi s = NULL)
: data(e), pred(p), succ(s) {} //默认构造器 ListNodePosi
insertAsPred( T const & e ); //前插入
ListNodePosi insertAsSucc( T const & e ); //后插入
};
2.列表:ADT接口
-
ADT接口
|操作接口 |功能 |适用对象 |
|–|–|–|
|size() |报告列表当前的规模(节点总数) |列表 |
|first(), last() |返回首、末节点的位置| 列表 |
|insertAsFirst(e), insertAsLast(e) |将e当作首、末节点插入 |列表|
|insert(p, e), insert(e, p) |将e当作节点p的直接后继、前驱插入 |列表|
|remove§ |删除位置p处的节点,返回其中数据项 |列表 |
|disordered() |判断所有节点是否已按非降序排列 |列表|
|sort() |调整各节点的位置,使之按非降序排列 |列表|
|find(e) |查找目标元素e,失败时返回NULL |列表|
|search(e)| 查找e,返回不大于e且秩最大的节点| 有序列|
|deduplicate(), uniquify() |剔除重复节点| 列表/有序列表|
|traverse() |遍历列表 |列表| -
List模板类
#include "listNode.h" //引入列表节点类
template<typename T> class List { //列表模板类
private:
int _size; ListNodePosi header, trailer; //哨兵 //头、首、末、尾节点的秩,可分别理解为-1、0、n-1、n
protected: /* ... 内部函数 */
public: /* ... 构造函数、析构函数、只读接口、可写接口、遍历接口 */
};
template void List::init() { //初始化,创建列表对象时统一调用
header = new ListNode;
trailer = new ListNode;
header->succ = trailer; header->pred = NULL;
trailer->pred = header; trailer->succ = NULL;
_size = 0;
}
3.循秩访问
- 重载下标操作符,可模仿向量的循秩访问方式
template<typename T> //assert: 0 <= r < size
T List::operator[]( Rank r ) const {
ListNodePosi p = first(); //从首节点出发
while ( 0 < r-- ) p = p->succ; //顺数第r个节点即是
return p->data; //目标节点
} //秩 == 前驱的总数
- 时间复杂度为O®;均匀分布时,期望复杂度为(1+2+3+…+n)/n=O(n)
三、无序列表
1.插入与删除
- 插入
template<typename T> //前插入算法(后插入算法完全对称)
ListNodePosi ListNode::insertAsPred( T const & e ) { //O(1)
ListNodePosi x = new ListNode( e, pred, this ); //创建
pred->succ = x; pred = x; //次序不可颠倒
return x; //建立链接,返回新节点的位置
} //得益于哨兵,即便this为首节点亦不必特殊处理——此时等效于insertAsFirst(e)
- 删除
template<typename T> //删除合法位置p处节点,返回其数值 T
List::remove( ListNodePosi p ) { //O(1)
T e = p->data; //备份待删除节点数值(设类型T可直接赋值)
p->pred->succ = p->succ; p->succ->pred = p->pred; //短路
delete p;
_size--;
return e; //返回备份数值
}
2.构造与析构
- copyNodes() + 构造
template<typename T>
void List::copyNodes( ListNodePosi p, int n ) { //O(n)
init(); //创建头、尾哨兵节点并做初始化
while ( n-- ) { //将起自p的n项依次作为末节点
insertAsLast( p->data );
p = p->succ;
}
}
- clear() + 析构
template<typename T>
List::~List() //列表析构 {
clear(); delete header; delete trailer;
} //清空列表,释放头、尾哨兵节点
template<typename T>
int List::clear() { //清空列表
int oldSize = _size;
while ( 0 < _size ) //反复删除首节点,O(n)
remove( header->succ );
return oldSize;
}
3.查找与去重
- 查找
template<typename T>
ListNodePosi List::find( T const & e, int n, ListNodePosi p ) const {
while ( 0 < n-- ) //自后向前
if ( e == ( p = p->pred ) ->data ) //逐个比对(假定类型T已重载“==”)
return p; //在p的n个前驱中,等于e的最靠后者
return NULL;
} //O(n)
- 去重
template<typename T> int List::deduplicate() {
int oldSize = _size;
ListNodePosi p = first();
for ( Rank r = 0; p != trailer; p = p->succ ) //O(n)
if ( ListNodePosi q = find( p->data, r, p ) ) //O(n)
remove ( q );
else r++; //无重前缀的长度
return oldSize - _size; //删除元素总数
} //正确性及效率分析的方法与结论,与Vector::deduplicate()相同
4.遍历
//函数指针
template<typename T> void List::traverse( void ( * visit )( T & ) ) {
for(NodePosi p = header->succ; p != trailer; p = p->succ)
visit( p->data );
}
//函数对象
template<typename T> template<typename VST> void List::traverse( VST & visit ) {
for( NodePosi p = header->succ; p != trailer; p = p->succ )
visit( p->data );
}
四、有序列表
1.唯一化
template<typename T>
int List::uniquify() {
if ( _size < 2 ) return 0; //平凡列表自然无重复
int oldSize = _size; //记录原规模
ListNodePosi p = first(); ListNodePosi q; //各区段起点及其直接后继
while ( trailer != ( q = p->succ ) ) //反复考查紧邻的节点对(p,q)
if ( p->data != q->data ) p = q; //若互异,则转向下一对
else remove(q); //否则(雷同)直接删除后者,不必如向量那样间接地完成删除
return oldSize - _size; //规模变化量,即被删除元素总数
} //只需遍历整个列表一趟,O(n)
2.查找
template<typename T>
ListNodePosi List::search( T const & e, int n, ListNodePosi p ) const {
do { p = p->pred; n--; } //从右向左
while ( ( -1 < n ) && ( e < p->data ) ); //逐个比较,直至命中或越界
return p; //失败时,返回区间左边界的前驱(可能是header)
}
- 性能 + 拓展
-
最好O(1),最坏O(n);等概率时平均O(n),正比于区间宽度
-
语义与向量相似,便于插入排序等后续操作:insert( search( e, r, p ), e )
-
按照循位置访问的方式,物理存储地址与其逻辑次序无关;依据秩的随机访问无法高效实现,而只能依据元素间的引用顺序访问
-
五、选择排序
1.代码
template<typename T> void List::selectionSort( ListNodePosi p, int n ) {
ListNodePosi head = p->pred, tail = p;
for ( int i = 0; i < n; i++ )
tail = tail->succ; //待排序区间为(head, tail)
while ( 1 < n ) { //反复从(非平凡)待排序区间内找出最大者,并移至有序区间前端
insert(remove( selectMax( head->succ, n ) ), tail ); //可能就在原地...
tail = tail->pred; n--; //待排序区间、有序区间的范围,均同步更新
}
}
template<typename T>
ListNodePosi List::selectMax( ListNodePosi p, int n ) { //Θ(n)
ListNodePosi max = p; //最大者暂定为p
for ( ListNodePosi cur = p; 1 < n; n-- ) //后续节点逐一与max比较
if ( ! lt( (cur = cur->succ)->data, max->data ) ) //data≥max
max = cur; //则更新最大元素位置记录
return max; //返回最大节点位置
}
2.稳定性
-
稳定性:有多个元素同时命中时,约定返回其中特定的某一个(比如最靠后者)
-
在这里,需要采用比较器
!lt
()或ge()
,从而等效于后者优先;若采用平移法,如此即可保证,重复元素在列表中的相对次序,与其插入次序一致
3.性能分析
- 共迭代n次,在第k次迭代中
- selectMax() 为Θ(n - k)
- swap()/remove() + insert() 为 O(1)
故总体复杂度应为Θ(n2)
-
尽管如此,元素的移动操作远远少于起泡排序;也就是说,Θ(n2)主要来自于元素的比较操作(实际更为费时,成本相对更低)
-
利用高级数据结构,selectMax()可改进至O(logn)
六、循环节
1.循环节
-
任何一个序列A[0,n),都可以分解为若干个循环节
-
任何一个序列A[0,n),都对应于一个有序序列S[0,n)
-
元素A[k]在S中对应的秩,记作r(A[k])=r(k)∈[0,n)
-
元素A[k]所属的循环节是:A[k],A[r(k)],A[r(r(k))],…,A[r(…(r(r(k))))]=A[k]
-
每个循环节,长度均不超过n
-
循环节之间,互不相交
2.单调性
- 采用交换法,每迭代一步,M都会脱离原属的循环节,自成一个循环节
- M原所属循环节,长度恰好减少一个单位;其余循环节,保持不变
3.无效的交换
-
M已经就位,无需交换
-
最初有c个循环节,就出现c次 —— 最大值为n,期望Θ(logn)
七、插入排序
1.减而治之
-
不变性
- 序列总能视作两部分: S[0, r) + U[r, n)
-
初始化:|S| = r = 0
-
反复地,针对e = A[r] 在S中查找适当位置,以插入e
2.代码
template<typename T> void List::insertionSort( ListNodePosi p, int n ) {
for ( int r = 0; r < n; r++ ) { //逐一引入各节点,由S 得到 r Sr+1
insert( search( p->data, r, p ), p->data );
p = p->succ;
remove( p->pred ); //转向下一节点
} //n次迭代,每次O(r + 1)
} //仅使用O(1)辅助空间,属于就地算法
- 紧邻于search()接口返回的位置之后插入当前节点,总是保持有序
3.平均性能:后向分析
-
e=[r]刚插入完成的那一时刻,此时的有序前缀[0,r]中其中的r+1个元素均有可能是e,且概率均为1/(r+1)
-
因此,在刚完成的这次迭代中为引入S[r]所花费时间的数学期望为 1 + ∑ k = 0 r k / ( r + 1 ) = 1 + r / 2 1+\sum_{k=0}^{r}k/(r+1) = 1+r/2 1+∑k=0rk/(r+1)=1+r/2
-
于是,总体时间的数学期望为 ∑ r = 0 n − 1 ( r / 2 + 1 ) = O ( n 2 ) \sum_{r=0}^{n-1}(r/2+1) = O(n^2) ∑r=0n−1(r/2+1)=O(n2)
八、归并排序
template<typename T> void List::mergeSort( ListNodePosi & p, int n ) {
if ( n < 2 ) return; //待排序范围足够小时直接返回,否则...
ListNodePosi q = p; int m = n >> 1; //以中点为界
for ( int i = 0; i < m; i++ ) q = q->succ; //均分列表:O(m) = O(n)
mergeSort( p, m ); mergeSort( q, n – m ); //子序列分别排序
p = merge( p, m, *this, q, n – m ); //归并
} //若归并可在线性时间内完成,则总体运行时间亦为O(nlogn)
template<typename T> ListNodePosi<T>
List::merge( ListNodePosi p, int n, List & L, ListNodePosi q, int m ) {
ListNodePosi pp = p->pred; //归并之后p或不再指向首节点,故需先记忆,以便返回前更新
while ( ( 0 < m ) && ( q != p ) ) //小者优先归入
if ( ( 0 < n ) && ( p->data <= q->data ) ) {
p = p->succ; n--;
} //p直接后移
else {
insert( L.remove( ( q = q->succ )->pred ) , p ) ); m--;
} //q转至p之前
return pp->succ; //更新的首节点
} //运行时间O(n + m),线性正比于节点总数
九、逆序对
1.逆序对(Inversion)
-
考查序列A[0, n),设元素之间可比较大小
- <i,j> is called an inversion if 0≤i<j<n and A[i]>A[j]
-
为便于统计,可将逆序对统一记到后者的账上
- I(j)=||{0≤i<j | A[i]>A[j] and hence <i,j> is an inversion}||
-
实例
- A[] = { 5, 3, 1, 4, 2 } 中,共有 0 + 1 + 2 + 1 + 3 = 7 个逆序对
- A[] = { 1, 2, 3, 4, 5 } 中,共有 0 + 0 + 0 + 0 + 0 = 0 个逆序对
- A[] = { 5, 4, 3, 2, 1 } 中,共有 0 + 1 + 2 + 3 + 4 = 10 个逆序对
-
显然,逆序对总数 I = ∑ j I ( j ) ≤ ( n 2 ) = O ( n 2 ) I=\sum_{j}{}I(j)≤{n \choose 2}=O(n^2) I=∑jI(j)≤(2n)=O(n2)
2.起泡排序
- 在序列中交换一对逆序元素,逆序对总数必然减少
-
在序列中交换一对紧邻的逆序元素,逆序对总数恰好减一
-
因此对于Bubblesort算法而言,交换操作的次数恰等于若共含 个逆序对,则输入序列所含逆序对的总数
3.插入排序
-
针对e=A[r]的那一步迭代恰好需要做I®次比较
-
若共含I个逆序对,则
-
关键码比较次数为O(I)
-
运行时间为O(n+I)
-
4.计数
- 任意给定一个序列,如何统计其中逆序对的总数?
- 蛮力算法需要O( n 2 n^2 n2)时间;而借助归并排序,仅需O( n log n n\log n nlogn)时间
十、游标实现
1.动机与构思
- 某些语言不支持指针类型,即便支持 频繁的动态空间分配也影响总体效率
- 利用线性数组,以游标方式模拟列表
- 维护逻辑上互补的列表data和free
2.实例