清华邓俊辉数据结构学习笔记(1) - 绪论、向量、列表

第一章 绪论

(a)计算

好算法正确(处理简单的、大规模的、一般性的、退化的、合法的输入)、健壮可读效率(速度快、存储空间小)
计算成本T(n):求解规模为n的问题所需基本操作数,在规模为n的所有实例中,只关注最坏(成本最高)者

(b)计算模型

图灵机模型(TM)(q, c; d, L/R, p):状态为q,当前字符为c;将当前字符改为d,转向左/右邻格;转入p状态,一旦转入状态’h’,停机,如(<, 1, 0, L, <)
RAM(Random Access Machine):寄存器顺序编号,每一操作只需常数时间

(c)大O记号

代表上界,根据T(n) < c.f(n),用f(n)代替T(n),可忽略常系数和低次项
O(1) 常数

//常数情况
2013×2013
//包含循环情况
for(i=0; i<n; i+= n/2013 + 1);
for(i=1; i<n; i=1 << i);
//包含分支
if((n+m)*(n+m) < 4*n*m) goto UNREACHABLE;
//包含递归
if(2==(n*n)% 5) 01(n);

O(logn) 对数:此类算法非常有效,复杂度无限接近常数,可忽略常底数和常数次幂
O(n) 线性及多项式:线性,从n到n2是编程题主要覆盖的范围
O(2^n) 指数:无效,从多项式到指数被认为有效算法到无效算法的分水岭

(d)算法分析

两个任务:正确性(不变性、单调性)+ 复杂度
复杂度分析:猜测+验证,迭代(级数求和),递归(递归跟踪+递推方程)
算数级数:与末项平方同阶
幂方级数:比幂次高出一阶
几何级数(a>1):与末项同阶
收敛级数:O(1)
调和级数:1+1/2+1/3+…+1/n=O(logn)
对数级数:log1+log2+…+logn=O(nlogn)

eg: 起泡排序

void bubblesort(int A[], int n){
	for (bool sorted = false; sorted = !sorted; n--)
		for (int i=1; i<n; i++)
			if (A[i-1] > A[i]){
				swap(A[i-1], A[i]);
				sorted = false;
			}
}

不变性:经k轮交换,最大的k个元素就位
单调性:经k轮交换,问题规模缩减至n-k
正确性:经至多n趟扫描后,算法必然终止且给出正确解答

(e)迭代与递归

减而治之:划分为两个子问题,其一平凡,另一缩减;

//数组求和
sum(int A[], int n){
	return
	(n<1)?
		0: sum(A, n-1) + A[n-1];	
}

求解sum(A, n)
递归基:sum(A, 0)
递归公式:T(n) = T(n-1) + O(1) T(0) = O(1)

//数组倒置
while(lo < hi) swap(A[lo++], A[hi--]);

分而治之:划分为两个子问题,规模大体相当

//数组求和
sum(int A[], int lo, int hi){
	if (lo == hi) return A[lo];
	int mi = (lo + hi) >> 1;
	return sum(A, lo, mi) + sum(A, mi + 1, hi)
}

求解sum(A, lo, hi)
递归基:sum(A, lo, lo)
递推公式:T(n) = 2*T(n/2) + O(1) T(1) = O(1)
复杂度:T(n) = O(n)

eg:从数组区间A[lo, hi)找出最大的两个整数

//Max2:迭代1
void max2(int A[], int lo, int hi, int & x1, int & x2){
	for (x1 = lo, int i = lo + 1; i < hi; i++)
		if (A[x1] < A[i]) x1 = i;
	for (x2 = lo, int i = lo + 1; i < x1; i++)
		if (A[x2] < A[i]) x2 = i;
	for (int i = x1 + 1; i < hi; i++)
		if (A[x2] < A[i]) x2 = i;
}

比较次数O(2n-3)

//Max2:迭代2
void max2(int A[], int lo, int hi, int & x1, int & x2){
	if (A[x1 = lo] < A[x2 = lo + 1]) swap(x1, x2);
	for(int i = lo + 2; i < hi; i++)
		if (A[x2] < A[i])
			if(A[x1] < A[x2 = i])
				swap(x1, x2);
}

最好情况:n - 1
最坏情况:2n - 3

//Max2:递归+分治
void max2(int A[], int lo, int hi, int & x1, int & x2){
	if (lo + 2 == hi){/*...*/;return;}
	if (lo + 3 == hi){/*...*/;return;}
	int mi = (lo + hi)/2;
	int x1L, x1R; max2(A, lo, mi, x1L, x2L);
	int x1R, x2R; max2(A, mi, hi, x1R, x2R);
	if(A[x1L] > A[x1R]){
		x1 = x1L; x2 = (A[x2L] > A[x1R])?x2L : x1R;
	}else{
		x1 = x1R; x2 = (A[x1L] > A[x2R])?x1L : x2R;
	}
}

比较次数:5n/3 - 2

(f)动态规划

eg1: 斐波那契数列:fib(n) = fib(n-1) + fib(n-2)

int fib(n){return (2 > n)? n : fib(n-1) + fib(n -2)}

低效,因为各递归实例均被大量重复地调用 O(2^n)
解决方法A:将计算过实例的结果制表备查
解决方法B:动态规划,将自顶向下递归,改为自底向上迭代

f = 0; g = 1;
while (0 < n--){
	g = g + f;
	f = g - f;
}
return g;

T(n) = O(n), 仅需O(1)空间
eg2:最长公共子序列(可能有多个;可能有歧义)

对于序列A[0, n]和B[0, m],LSC有三种情况:
(1)n = -1或m = -1,取空序列(“”)
(2)A[n] = ‘X’ = B[m],取LSC(A[0, n),B[0, m)) + ‘X’ (减而治之)
(3)A[n] ≠ B[m],则在 LCS(A[0, n], B[0, m))与 LCS(A[0, n), B[0, m])中取更长者 (分而治之)

最好情况:O(n + m)
最坏情况:O(2^n) (n = m)

与斐波那契数列一样,有大量重复的递归实例;若采用动态规划,只需O(nm)时间即可计算所有子问题,为此只需(1)将所有子问题假想列成一张表;(2)颠倒计算方向,从LCS(A[0], B[0])出发依次计算所有项。

第二章 向量

(a)接口与实现

size() - 报告当前向量的规模(元素总数)
get® - 获取秩为r的元素
put(r,e) - 用e替换秩为r元素的数值
insert(r,e) - e作为秩为r元素插入,原后继元素依次后移
remove® - 删除秩为r的元素,返回该元素中原存放的对象
disordered() - 判断所有元素是否已按非降序排列
sort() - 调整各元素的位置,使之按非降序排列
find(e) - 查找目标元素e
search(e) - 查找目标元素e,返回不大于e且秩最大的元素
deduplicate() - 删除重复元素
uniquify() - 删除重复元素(有序向量)
traverse() - 遍历向量并统一处理所有元素,处理方法由函数对象指定

vector模板类

typedef int Rank;
#define DEFAULT_CAPACITY 3
template <typename T> class Vector{
	private:Rank _size; int _capacity; T* _elem;
	protected:
		/*...内部函数*/
	public:
		/*...构造函数*/
		/*...析构函数*/
		/*...只读接口*/
		/*...可写接口*/
		/*...遍历接口*/
}

构造与析构

Vector(int c = DEFAULT_CAPACITY)
	{ _elem = new T[_capacity = c]; _size = 0;} //默认
Vector(T const *A, Rank lo, Rank hi) //数组区间复制
	{ copyFrom(A, lo, hi); }
Vector(Vector<T> const& V, Rank lo, Rank hi)
	{ copyFrom(V._elem, lo, hi); } //向量区间复制
Vector(Vector<T> const& V)
	{ copyFrom(V._elem, 0, V._size); }
~Vector() { delete [] _elem; } //释放内部空间

基于复制的构造

template <typename T> //T为基本类型,或已重载复制操作符'='
void Vector<T>::copyFrom(T* const A, Rank lo, Rank hi){
	_elem = new T[_capacity = 2*(hi - lo)];  //分配空间
	_size = 0; //规模清零
	while (lo < hi) //A[lo, hi)内的元素逐一
		_elem[_size++] = A[lo++]; //复制至_elem[0,hi-lo)

}

(b)可扩充向量

静态空间管理:开辟内部数组_elem[]并使用一段地址连续的物理空间,_capacity总容量固定,则有明显不足:
(1)上溢:_elem[]不足以存放所有元素,尽管此时系统仍有足够的空间
(2)下溢:_elem[]中的元素寥寥无几,装填因子小于50%
动态空间管理:在即将上溢时,扩大内部数组的容量
扩容算法实现

template <typename T>
void Vector<T>::expand(){
	if (_size < _capacity) return;
	_capacity = max(_capacity, DEFAULT_CAPACITY);
	T* oldElem = _elem; _elem = new T[_capacity <<= 1]; //容量加倍
	for(int i=0; i<_size;i++) //复制原向量内容
		_elem[i] = oldElem[i];
	delete [] oldElem;
}

容量递增策略

T* oldElem = _elem; _elem = new T[_capacity += INCREMENT]; //追加固定大小的容量

最坏情况:在初始容量为0的空向量中,连续插入n=m*I >>2个元素,在1、I+1、2I+1、3I+1次插入时需要扩容,各次扩容需要的时间成本为0,I, 2I, …, (m-1)I,总体耗时O(n^2),每次扩容分摊成本O(n)

容量加倍策略

T* oldElem = _elem; _elem = new T[_capacity <<= 1];

最坏情况:在初始容量为1的满向量中,连续插入n=2 ^ m>>2个元素,在第1、2、4、8、16次插入都需要扩容,每次扩容中复制原向量的时间成本分别为1,2,4,8,…,2^m = n,总体耗时O(n),每次扩容的分摊成本为O(1)
平均分析复杂度:根据数据结构各种操作出现概率的分布,将对应的成本加权平均,各种可能的操作作为独立事件分别考查,割裂了操作之间的相关性和连贯性,不能准确评判数据结构和算法的真实性能;
分摊复杂度:对数据结构连续实施足够多次操作,所需总成本分摊至单次操作,对一系列操作做整体的考量

(c)无序向量

元素访问
利用 V.get® 和 V.put(r, e) 接口读写向量元素没有数组A[r]便捷高效,需要重载下标操作符[]

template <typename T>
T & Vector<T>::operator[](Rank r) const{return _elem[r];}

此后对外V[r]对应内部V._elem[r]
右值:T x = V[r] + U[s] * W[t];
左值:V[r] = (T)(2*x + 3)
插入

template <typename T> //e作为秩为r元素插入
Rank Vector<T>::insert(Rank r, T const & e){
	expand(); //如有必要,扩容
	for (int i=_size; i>r; i--)
		_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++]; //[hi,_size)顺次前移hi-lo位
	_size = lo; shrink(); //更新规模,如有必要则缩容
	return hi-lo; //返回被删除元素的数目
}

查找
无序向量:T为可判等的基本类型,或已重载操作符"==“或”!="
有序向量:T为可比较的基本类型,或已重载操作符"<“或”>"

template <typename T>
Rank Vector<T>::find(T const & e, Rank lo, Rank hi) const{
	while((lo < hi--) && (e!=_elem[hi]));
	return hi;
}

复杂度输入敏感,最好O(1),最差O(n)
单元素删除
可以视作区间删除操作的特例:[r] = [r, r+1)

template <typename T> //删除向量中秩为r的元素
T Vector<T>::remove(Rank r){
	T e = _elem[r]; //备份被删除元素
	remove(r,r+1);
	return e;
}

Q:能否反复调用remove®实现remove(lo,hi)呢?
每次循环耗时正比于删除区间的后缀长度 = n - hi = O(n),循环次数等于区间宽度=hi - lo = O(n),如此将导致O(n^2)的复杂度。

(d1)有序向量:唯一化

应用实例:网络搜索局部结果经过去重操作,形成最终汇报
错误版

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;
}

正确性:
1、不变性。在当前元素V[i]的前缀V[0, i)中,各元素彼此互异,初始i=1时自然成立。
2、单调性。随着反复while迭代,前缀增长,后缀下降,算法最多迭代O(n)轮。

时间复杂度:
每轮迭代中find()和remove()累计消耗线性时间,故总体为O(n^2)

进一步优化:
1、仿照uniquify()高效版,元素移动次数降至O(n)——但比较次数仍为O(n^2);
2、先对需删除的元素标记,再统一删除——稳定性保持,但因查找长度更长,导致更多的比对操作;
3、V.sort().uniquify():简明实现最优的O(nlogn)

遍历
遍历向量,统一对各元素实施visit操作
利用函数指针机制,只读或局部性修改

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]);}

实例:统一将向量中所有元素分别加一
首先,实现一个可使单个T类型元素加一的类

template <typename T> //假设T可直接递增或已重载操作符
struct Increase{ //函数对象:通过重载操作符()实现
	virtual void operator()(T & e){e++;} //加一
}

此后

template <typename T> void increase(Vector<T> & V){
	V.traverse(Increase<T>()); //基本操作遍历向量
}

有序性及其甄别
相邻逆序对的数目,可用以度量向量的逆序程度

template <typename T>
int Vector<T>::disordered() const{
	int n = o;
	for (int i = 1; i < _size; i++) //逐一检查各对相邻元素
		n += (_elem[i-1] > _elem[i]); //逆序则计数
	return n; //向量有序当且仅当n = 0
} //若只需判断是否有序,则首次遇到逆序对后可立即终止

独特性
低效算法
观察:在有序向量中,重复的元素必然相互紧邻构成一个区间,每个区间只需保留单个元素

template <typename T> int Vector<T>::uniquify(){
	int oldSize = _size; int i = 0;
	(_elem[i] == _elem[i+1]) ? remove(i+1); i++;
	return oldSize - _size; //删除元素总数
}

复杂度:运行时间主要取决于while循环,次数共计n-1;最坏情况下每次都要调用remove(),耗时O(n-1) ~ O(1),累计O(n^2)。尽管省去find(),总体与无序向量的deduplicate()相同。
反思:低效的根源在于,同一元素可作为被删除元素的后继多次前移,若以重复区间为单位,成批删除雷同元素,性能必将改进。
高效算法

template <typename T> int Vector<T>::uniquify(){
	Rank i = 0, j = 0; //各对互异相邻元素的秩
	while (++j < _size) //逐一扫描,直至末元素
		//跳过雷同者,发现不同元素时,向前移至紧邻于前者右侧
		if (_elem[i]!=_elem[j]) _elem[++i]=_elem[j];
	_size = ++i; shrink(); //直接截除尾部多余元素
	return j-i; //被删除元素总数
}

复杂度:共计n-1次迭代,每次常数时间,累计O(n)时间

(d2)有序向量:二分查找

统一接口

template <typename T> //查找算法统一接口
Rank Vector<T>::search(T const & e, Rank lo, Rank hi) const{
	return (rand() % 2)? //各按50%的概率随机选用
		binSearch(_elem, e, lo, hi) //二分查找法或
	:	fibSearch(_elem, e, lo, hi); //Fibonacci查找法
}

问题:如何处置特殊情况?比如目标元素不存在,或目标元素存在多个
语义约定:至少应该便于有序向量自身的维护V.insert(1 + V.search(e), e),即使失败也应给出新元素适当的插入位置,若允许重复元素,每一组也需按其插入的次序排列。
约定:在有序向量区间V[lo,hi)中,确定不大于e的最后一个元素(秩)
若-∞< e < V[lo],则返回lo - 1 (左侧哨兵)
若V[hi - 1] < e < +∞,则返回hi - 1 (末元素=右侧哨兵紧邻)
版本A:
减而治之:任一元素x = S[mi]为界,可将查找区间分为三部分,S[lo, mi) <= S[mi] <= S(mi, hi),S[mi]称作轴点,经过至多两次比较,或者能够命中,或者把问题规模缩减至一半

template <typename T> 
static Rank binSearch(T* A, T const& e, Rank lo, Rank hi){
	while(lo < hi){
		Rank mi = (lo + hi) >> 1; //以中点为轴点
		if (e < A[mi]) hi = mi;
		else if (A[mi] < e) lo = mi + 1;
		else return mi; //在mi处命中
	}
	return -1; //查找失败
}

线性递归:T(n) = T(n/2) + O(1) = O(logn),大大优于顺序查找,递归深度O(logn),各递归实例耗时O(1)。
查找长度:更精细地评估查找算法的性能,考查关键码的比较次数,即查找长度

(d3)有序向量:Fibonacc查找

思路:转向左、右分支前的关键码比较次数不等,而递归深度相同。若能通过递归深度的不均衡,对转向成本的不均衡进行补偿,平均查找深度应该能进一步缩短。
比如n = fib(k) - 1,可取mi = fib(k -1) - 1,于是前后子向量的长度分别为fib(k - 1) - 1、fib(k - 2) - 1

template <typename T>
static Rank fibSearch(T* A, T const & e, Rank lo, Rank hi){
	Fib fib(hi - lo); //创建Fib数列
	while (lo < hi){
		while (hi - lo < fib.get()) fib.prev(); //至多迭代几次?
			//通过向前顺序查找,确定Fib(k) - 1的轴点
		Rank mi = lo + fib.get() - 1; 
		if (e < A[mi]) hi = mi;
		else if (A[mi] < e) lo = mi + 1;
		else return mi;
	}
	return -1;
}

(d4)有序向量:二分查找(改进)

改进思路:每次迭代仅做一次关键码比较,所有分支只有2个方向

template <typename T> static Rank binSearch(T* A, T const & e, Rank lo, Rank hi){
	while(1 < hi - lo){
		Rank mi = (lo + hi) >> 1;
		(e < A[mi]) ? hi = mi : lo = mi; //[lo,mi)或[mi,hi)
	}
	return (e == A[lo])? lo:-1
}

(d5)有序向量:插值查找

语义约定:search()接口约定,返回不大于e的最后一个元素。只有兑现这一约定,才能有效支持算法,比如V.insert(1+V.search(e), e)
(1)当多个命中元素时,必须返回最靠后(秩最大)者;
(2)失败时,应返回小于e的最大者(含哨兵[lo - 1])

template <typename T> static Rank binSearch(T* A, T const& e, Rank lo, Rank hi){
	while (lo < hi){
		Rank mi = (lo + hi) >> 1;
		(e < A[mi])? hi = mi : lo = mi + 1;
	} //出口时,A[lo = hi]为大于e的最小元素
	return --lo; //lo - 1为不大于e的元素的最大秩
}

待查找区间缩短至0而非1时算法才结束;转入右侧子向量时,左边界取作mi+1而非mi;无论成功与否,返回的秩严格符合接口的语义约定。

(e)起泡排序

排序器:统一入口

template <typename T>
void Vector<T>::sort(Rank lo, Rank hi){
	switch (rand() % 5){
		case 1 : bubbleSort(lo, hi); break; //起泡排序
		case 2 : selectionSort(lo, hi); break; //选择排序
		case 3 : mergeSort(lo, hi): break; //归并排序
		case 4 : heapSort(lo, hi);break; //堆排序(第十章)
		default : quickSort(lo, hi);break; //快速排序(第十二章)
	}
}

起泡排序

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;
			swap(_elem[lo - 1], _elem[lo]); //交换
		}
	return sorted; //返回有序标志
}//乱序限于[0,根号n)时,仍需O(n的3/2方)时间:O(n.r)

改进:前一版本的逻辑型标志sorted,改为秩last

template <typename T> void Vector<T>::bubbleSort(Rank lo, Rank hi)
{ while (lo < (hi = bubble(lo,hi)));}

template <typename T> Rank Vector<T>::bubble(Rank lo, Rank hi){
	Rank last = lo; //最右侧的逆序对初始化为[lo - 1, lo]
	while (++lo < hi) //自左向右逐一检查各对相邻元素
		if (_elem[lo - 1] > _elem[lo]){//若逆序
			last = lo; //更新最右侧逆序对的位置
			swap(_elem[lo - 1], _elem[lo]); 
		}
	return last;
}

综合评价
(1)效率与第一章针对整数数组的版本相同,最好O(n),最坏O(n^2);
(2)输入重复元素时,算法的稳定性(输入、输出序列的相对次序)保持不变;
(3)在起泡排序中,元素a和b的相对位置发生变化,只有一种可能:经分别与其他元素的交换,二者相互接近直至相邻,在接下来的一轮扫描交换中,二者因逆序而交换位置。

(f)归并排序

原理:分治策略(向量和列表通用) 运行时间O(nlogn)
序列一分为二 //O(1)
子序列递归排序 //2×T(n/2)
合并有序子序列 //O(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); //归并
}

二路归并原理:将两个有序序列合并为一个有序序列S[lo, hi) = S[lo, mi) + S[mi, hi)

template <typenamee T> void Vector<T>::merge(Rank lo, Rank mi, Rank hi){
	T* A = _elem + lo;  //合并后的子向量A[0, hi - lo) = _elem[lo, hi)
	int lb = mi - lo; T* B = new T[lb]; //前子向量B[0, lb) = _elem[lo,mi)
	for (Rank i = 0; i < lb; B[i] = A[i++]); //复制前子向量B
	int lc = hi - mi; T* c = _elem + mi; //后子向量C[0,lc) = _elem[mi, hi)
	for (Rank i = 0, j = 0, k = 0; (j <lb) || (k < lc);){//B[j]和C[k]中的小者转至A的末尾
		if ((j < lb) && (lc <= k || (B[j] <= C[k]))) A[i++] = B[j++]; //C[k]已无或不小
		if ((k < lc) && (lb <= j || (C[k] < B[j]))) A[i++] = C[k++]; //B[j]已无或更大
	}//该循环实现紧凑,但效率不如拆分处理
delete [] B; //释放临时空间B
}

复杂度分析:算法的运行时间主要消耗于for循环,共有两个控制变量j和k,初始j=0,k=0;最终j=lb, k=lc;即j + k = lb + lc = hi - lo = n。
观察每经过一次迭代,j和k中至少有一个会加一(j+k也至少加一),故merge()总体迭代不超过**O(n)**次,累计线性时间。这一结论与排序算法的Ω(nlogn)下届并不矛盾,因为B和C已经各自有序。
注意:待归并子序列不必等长,允许lb ≠ lc, mi ≠ (lo + hi) / 2,这一算法和结论也适用于另一类序列——列表。

第三章 列表

(a)接口与实现

根据是否改变数据结构,所有操作方式大致分为:(1)静态:仅读取,数据结构的内容及组成一般不变,get、search;(2)动态:需写入,数据结构的局部或整体将改变,insert、remove。
数据元素的存储与组织方式:(1)静态:数据元素的物理存储次序与其逻辑顺序严格一致,可支持高校静态操作,如向量;(2)动态:为各数据元素动态分配和回收的物理空间,逻辑上相邻的元素记录彼此物理地址,逻辑上形成一个整体,可支持高效的动态操作,如列表。
列表元素称为节点,相邻节点彼此互称前驱后继,若存在则唯一,若无前驱/后继的唯一节点为首/末节点。
向量:循秩访问,O(1)时间确定物理地址
列表:循位置访问,利用节点间的相互引用
ADT接口(ListNode)

pred() //当前节点前驱节点的位置
succ() //当前节点后继节点的位置
data() //当前节点所存数据对象
insertAsPred(e) //插入前驱节点,存入被引用对象e,返回新节点位置
insertAsSucc(e) //插入后继节点,存入被引入对象e,返回新节点位置

列表节点:ListNode模板类

#define Posi(T) ListNode<T>* //列表节点位置
template <typename T> //简洁起见,完全开放,不过度封装
struct ListNode{//列表节点模板类(双向链表形式)
	T data; //数值
	Posi(T) pred; //前驱
	Posi(T) succ; //后继
	ListNode(){} //针对header和trailer的构造
	ListNode(T e, Posi(T) p = NULL, Posi(T) s = NULL):data(e),pred(p),succ(s){}//默认构造器
	Posi(T) insertAsPred(T const& e);//前插入
	Posi(T) insertAsSucc(T const& e);//后插入
}

其他ADT接口

size() //报告列表当前规模(节点总数)
first(),last() //返回首、末节点的位置
insertAsFirst(e),insertAsLast(e) //把e当作首、末节点插入
insertBefore(p,e),insertAfter(p,e) //将e当作节点p的直接前驱、后继插入
remove(p) //删除位置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; Posi(T) header; Posi(T) trailer;//头、尾哨兵
protected: /*...内部函数 */
public: /*...构造函数、析构函数、只读接口、可写接口、遍历接口/
}

构造

template <typename T> void List<T>::init(){
	header = new ListNode<T>; //创建头哨兵节点
	trailer = new ListNode<T>; //创建尾哨兵节点
	header->succ = trailer; header->pred = NULL; //互联
	trailer->pred = header; trailer->succ = NULL;//互联
	_size = 0;//记录规模
}

(b)无序列表

从秩到位置,模仿向量循秩访问,重载下标操作符

template <typename T>
T List<T>::operator[](Rank r) const{
	Posi(T) p = first(); //从首节点出发
	while(0 < r--) p = p->succ; //顺数第r个节点
	return p->data;//目标节点
}//任一节点的秩,亦即其前驱总数

查找:在节点p(可能是trailer)的n个(真)前驱中,找到等于e的最后者

template <typename T> //从外部调用时,0<=n<=rank(p)<_size
Posi(T) List<T>::find(T const & e, int n, Posi(T) p) const{
	while(0 < n--) //从右向左,逐个将p的前驱与e比对
		if( e == ( p = p->pred )->data) return p; //直至命中或范围越界
	return NULL; //若越出左边界,意味查找失败
}//header的存在使处理更为简洁

插入

template <typename T> Posi(T) List<T>::insertBefore(Posi(T) p, T const& e)
{ _size++; return p->insertAsPred(e);} //e当作p的前驱插入
template <typename T> //前插入算法(后插入算法对称)
Posi(T) x = new ListNode(e, pred, this); //创建(耗时100倍)
pred->succ = x; pred = x; return x; //建立连接,返回新节点的位置

基于复制的构造

template <typename T>//基本接口
void List<T>::copyNodes(Posi(T) p, int n){ //O(n)
	init();//创建头、尾哨兵节点并做初始化
	while (n--) //将起自p的n项依次作为末节点插入
		{insertAsLast(p->data); p = p->succ;}
}

删除

template <typename T> //删除合法位置p处节点,返回其数值
T List<T>::remove(Posi(T) 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> List<T>::~List() //列表析构
{clear();delete header; delete trailer;} //清空列表,释放头、尾哨兵节点
template <typename T> int List<T>::clear(){//清空列表
	int oldSize = _size;
	while(0 < _size) //反复剔除首节点,直至列表为空
		remove(header->succ);
	return oldSize;
} //O(n),线性正比于列表规模

唯一化

template <typename T> int List<T>::deduplicate(){//剔除无序列表中的重复节点
	if (_size < 2) return 0; //平凡列表无重复
	int oldSize = _size; //记录规模
	Posi(T) p = first(); Rank r = 1; //p从首节点起
	while (trailer != (p = p->succ)){//依次直到末节点
		Posi(T) q = find(p->data, r, p); //在p的r个(真)前驱中,查找与之雷同者
		q ? remove(q) : r++; //若的确存在则删除之,否则秩递增
	}
	return oldSize - _size; //列表规模变化量,即被删除元素总数
}//正确性即效率分析方法和结论,与Vector::deduplicate()相同

(c)有序列表

唯一化

template <typename T> int List<T>::uniquify(){//成批剔除重复元素
	if( _size < 2) return 0; //平凡列表无重复
	int oldSize = _size; //记录原规模
	ListNodePosi(T) p = first(); ListNodePosi(T) q; //p为各区段起点,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> //在有序列表内节点p的n个(真)前驱中,找到不大于e的最后者
Posi(T) List<T>::search(T const & e, int n, Posi(T) p) const{
	while (0 <= n--) //对于p的最近的n个前驱,从右到左
		if(((p = p->pred ) -> data) <= e) break; //逐个比较
	return p; //直至命中、数值越界或范围越界后,返回查找终止的位置
}//最好O(1),最坏O(n);等概率时平均O(n),正比于区间宽度

(d)选择排序

selectionSort实现:对列表中起始于位置p的连续n个元素做选择排序

template <typename T> void List<T>::selectionSort(Posi(T) p, int n){
	Posi(T) head = p->pred; Posi(T) tail = p; //待排序区间(head,tail)
	for(int i = 0; i < n; i++) tail = tail->succ;//head/tail可能是头/尾哨兵
	while (1 < n) {//反复从非平凡待排序区间找出最大者,移至有序区间前端
		insertBefore(tail, remove(selectMax(head->succ,n)));
		tail = tail->pred; n--; //待排序区间、有序区间的范围,均同步更新
	}
}

selectMax实现:从起始于位置p的n个元素中选出最大者,1<n

template <typename T> 
Posi(T) List<T>::selectMax(Posi(T) p, int n){ //O(n)
	Posi(T) max = p; //最大者暂定为p
	for (Posi(T) cur = p; 1 < n; n--) //后续节点逐一与max比较
		if( !lt((cur = cur->succ)->data, max->data)) //若>=max
			max = cur; //更新最大元素位置记录
	return max; //返回最大节点位置
}

性能:共迭代n次,在第k次迭代中selectMax()为O(n-k),remove()和insertBefore()均为O(1),故总体复杂度为O(n^2) 。元素移动操作远远少于起泡排序,O(n^2)主要来自于元素比较操作。

(e)插入排序

将序列看成两部分:Sorted + Unsorted ,即L[0, r) + L[r, n)
初始化:|S| = r = 0 //空序列无所谓有序
迭代:关注并处理e = L[r],在S中确定适当位置插入e,得到有序的L[0,r] //有序序列的插入
不变性:随着r的递增,L[0,r)始终有序,直到r=n, L即整体有序
insertionSort实现:对列表中起始于位置p的连续n个元素做插入排序,valid( p ) && rank( p ) + n <= size

template <typename T> void List<T>::insertionSort(Posi(T) p, int n){
	for (int r = 0; r < n;r++){//逐一引入各节点,由Sr,得到Sr+1
		insertAfter(search( p->data, r, p),p->data); //查找+插入
		p = p->succ; remove( p->pred); //转向下一节点
	}//n次迭代,每次O(r+1)
}//仅使用O(1)辅助空间,属于就地算法

平均性能

  • 假定各元素的取值遵守均匀独立分布,平均要做多少次元素比较?
  • 考查L[r]刚插入完成的那一时刻穿越,试问此时的有序前缀L[0,r]中,哪个元素是此前的L[r]?
  • 其中的r+1个元素均有可能,且概率均等于1/(r+1),因此在刚完成的这次迭代中,为引入S[r]所花费时间的数学期望为[r+(r-1)+…3+2+1+0]/(r+1)+1=r/2+1,总体数学期望=[0+1+…+(n-1)]/2+1=O(n^2)
清华邓俊辉数据结构学习笔记(1) - 绪论、向量、列表清华邓俊辉数据结构学习笔记(1) - 绪论、向量、列表 王海海 发布了2 篇原创文章 · 获赞 0 · 访问量 38 私信 关注
上一篇:CodeForces - 1251E2 (思维+贪心)


下一篇:排序 起泡排序(bubble sort),归并排序(merge sort)