根据是否修改数据结构,所有操作大致分为两类方式
与操作方式相对应地,数据元素的存储与组织方式也分为两种
列表(list)是采用动态储存策略的典型结构
相邻节点彼此互称前驱(predecessor)或后继(successor)
作为列表的基本元素 列表节点首先需要 独立地“封装”实现
基本操作接口
|操作接口 |功能 |
|–|–|
|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 ); //后插入
};
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;
}
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; //目标节点
} //秩 == 前驱的总数
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; //返回备份数值
}
template<typename T>
void List::copyNodes( ListNodePosi p, int n ) { //O(n)
init(); //创建头、尾哨兵节点并做初始化
while ( n-- ) { //将起自p的n项依次作为末节点
insertAsLast( p->data );
p = p->succ;
}
}
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;
}
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()相同
//函数指针
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 );
}
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)
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 )
按照循位置访问的方式,物理存储地址与其逻辑次序无关;依据秩的随机访问无法高效实现,而只能依据元素间的引用顺序访问
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; //返回最大节点位置
}
稳定性:有多个元素同时命中时,约定返回其中特定的某一个(比如最靠后者)
在这里,需要采用比较器!lt
()或ge()
,从而等效于后者优先;若采用平移法,如此即可保证,重复元素在列表中的相对次序,与其插入次序一致
故总体复杂度应为Θ(n2)
尽管如此,元素的移动操作远远少于起泡排序;也就是说,Θ(n2)主要来自于元素的比较操作(实际更为费时,成本相对更低)
利用高级数据结构,selectMax()可改进至O(logn)
任何一个序列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
循环节之间,互不相交
M已经就位,无需交换
最初有c个循环节,就出现c次 —— 最大值为n,期望Θ(logn)
不变性
初始化:|S| = r = 0
反复地,针对e = A[r] 在S中查找适当位置,以插入e
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)辅助空间,属于就地算法
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),线性正比于节点总数
考查序列A[0, n),设元素之间可比较大小
为便于统计,可将逆序对统一记到后者的账上
实例
显然,逆序对总数 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)
在序列中交换一对紧邻的逆序元素,逆序对总数恰好减一
因此对于Bubblesort算法而言,交换操作的次数恰等于若共含 个逆序对,则输入序列所含逆序对的总数
针对e=A[r]的那一步迭代恰好需要做I®次比较
若共含I个逆序对,则
关键码比较次数为O(I)
运行时间为O(n+I)