清华大学 Data Structures and Algorithms (I)

Week 4 向量(上)

Binary Search 二分查找

实现

template <typename T>
static Rank binSearch(T* A, T const &e, Rank lo , Rank hi){
	while(lo < hi){
		Rank mi = (lo + hi ) >> 1; // Rank mi = (lo + hi) / 2;
		if( e < A[mi] ){ // 小于号便于理解,与从小到大次序吻合
			hi = mi};
		else if ( A[mi] < e ){
			lo = mi + 1};
		else{
			return mi;
		}
	}
		return -1; // 查找失败
}
		

复杂度

线性递归:T(n) = T(n/2) + O(1) = O(logn)
递归跟踪:轴点总取终点,递归深度O(logn),各递归实例均耗时O(1)

Quiz

V = {2,3,5,7,11,13,17}; V.search(16,0,7) 需要进行几次比较?

5 (但如果不考虑lo和hi的比较,并且while的判断是lo<hi的时候,个人认为是4次?)
向左侧转向只需一次比较,向右侧转向需要两次比较

查找长度

如何更精细地评估查找算法的性能?
考察关键码的比较次数,即查找长度

查找长度均大致为O(1.5logn)

Week 5 向量(下)

有序向量查找 Fibonacci查找

二分有序向量查找向左和向右比较次数不相等,而递归深度却相同

设 n = fib(k) - 1, 则可取 mi = fib(k-1) - 1
于是,前后子向量的长度为别为 fib(k-1)-1, fib(k-2)-1

template <typename T>
static Rnak fibSearch(T* A, T const & e, Rnak lo , Rank hi){
	Fib fib(hi-lo);
	while (lo < hi){
		while (hi - lo < fib.get()) fib.prev();
		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; // 查找失败
}

最优性

通用策略:对于任何的A[0,n),总是选取A[\lambda n]为轴点,0<= \lambda < 1
for binary search, \lambda = 0.5, for Fibonaccis search, \lambda = \phi = 0.618
In the range of [0,1), suppose the average search length is \alpha(\lambda)log_2(n)

We get:

a(入)log2n=入(1+a(入)log2(入n)]+(1-入)[2+a(入)log2((1-入)n)]
当\lambda = \phi 时,a(入)=1.440420 达到最小

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

构思

使用 if, else if, 让代码分支只有两个方向
同样以mid 作为轴点
e < mid, 则e若存在必属于S[lo,mi]
mid <= e, 则e若存在必属于S[mi,hi)
只有当元素数目 hi-lo = 1时才能得知是否命中
出口时 hi=lo+1;查找区间仅含A[lo]

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;
	}
	return(e == A[lo])? lo:-1;

相较于之前三个分支的版本,最好情况下更坏,最坏情况下更好,各种情况下的search length 更为接近,整体性能趋于稳定
对于规模为n的向量,二分查找版本和改进版本最优时间的复杂度分别为 O(1) 和 O(log_2n)

语意

search()接口的返回值:返回不大于e的最后一个元素

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

正确性:左侧<=e, 右侧 > e
对于这个版本,当查找区间的长度缩小为0时,V[lo]是 e <V[r],因此返回 lo - 1;

有序向量差值查找

均匀且独立的随机分布
[lo,hi)内个元素应大致按照线性趋势增长

\frac{mi-lo}{hi-lo} = \frac{e-A[lo]}{A[hi]-A[lo]}

通过猜测轴点mi,极大提高收敛速度

mi \sim lo+(hi-lo)\dot\frac{e-A[lo]}{A[hi]-A[lo]}

因为采用差值估算的方法,也被称为差值查找

性能分析

平均情况:每经过一次比较,n缩至\sqrt(n)
所以为O(loglogn) < O(log n)

字宽折半

n -> log(n)
\sqrt(n) -> 0.5*log(n)

左边折半复杂度为log(n), 右边为复杂度为log(n), 因此为log(log(n)).

综合对比

从O(log(n))到O(log(log(n))) 除非查找区间宽度极大或者比较操作成本极高,通常优势并不明显。
实际可行的方法 大规模:插值查找,中规模:折半查找,小规模:顺序查找。

向量起泡排序

排序器

统一入口

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; //堆排序
		case 5 : quickSort(lo,hi); break; //快速排序
	}
}

改进

起泡排序改进策略:
在每一趟扫描交换中,都记录下是否存在逆序元素,即做过扫描交换

template <typename T> void Vector<T>::bubbleSort(Rank lo, Rank hi){
	while(!bubble(lo,hi--));
}
template <typename T> void 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;
}

经改进的起泡排序在什么情况下会提前结束?

完成的扫描交换趟数 = 实际发生元素交换的扫描交换趟数 + 1
即:在新的一轮扫描交换中,没有发生元素交换

改进反例

如果乱序元素集中于vector前面,扫描交换的趟数不会超过乱序前缀的个数r,因此为O(n*r).
if r = \sqrt(n); O(n^1.5)

再改进

template <typename T> void Vector<T>::bubbleSort(Rank lo, Rnak hi){
	while(lo<(hi=bubble(lo,hi)));
}
template <typename T> void Vector<T>::bubble(Rank lo, Rank hi){
	Rank last = lo;
	while(++lo < hi){
		if (_elem[lo-1] > _elem[lo]){
			last = lo;
			swap(_elem[lo-1],_elem[lo]);
		}
	}
	return last;
}

如果前面的乱序向量长度为\sqrt(n),则时间复杂度为O(n)

综合评价

最好O(n), 最坏O(n^2)

归并排序

  1. 无序向量的递归分解
  2. 有序向量的逐层归并

T(n) = 2T(n-1)+O(n)
O(nlogn)

主算法

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,mo) + S[mi, hi)

template <typename T> void Vector<T>::merge(Rank lo, Rank hi){
	T* A = _elem+lo;
	int lb = mi - lo; T* B = new T[lb];
	int lc = hi - mi; T* C = _elem + mi;
	for (Rank i = 0 ; i < lb; B[i] = A[i++]);
	for (Rank i = 0 ; j = 0; k = 0; (j<lb) || (k<lc);){
		if((j<lb) && ((lc <= k ) || (B[j] <= C[k]))) A[i++] = B[j++];
		if((k<lc) && ((lb <= j ) || (C[k] <= B[j]))) A[i++] = C[k++];
	}
	delete [] B;
}

正确性

考虑C后部元素,删除冗余逻辑

for (Rank i = 0; j = 0; k = 0; j < lb;){
	if ((k<lc) && C[k] < B[j]) A[i++] = C[k++];
	if ((lc <= k || B[j] < C[k]) A[i++] = B[j++];
}

性能分析

原先考虑 (j<lb) || (k <lc);
起初 j = 0; k = 0
最终 j =lb; k = lc
j+k=n
merge()总体迭代不过O(n)次。
对于归并排序,最好最坏的情况都为O(nlog(n))

Week 6 列表

接口与实现

列表是采用动态储存策略的典型结构,其中的元素称作节点(node)
列表应改用循位置访问 (call-by-position)
ADT接口:

pred()
succ()
data()
insertAsPred(e)
insertAsSucc(e)
#define Posi(T) ListNode<T>*
template <typename T>
struct ListNode{
	T data;
	Posi(T) pred;
	Posi(T) succ;
	ListNode(){} //constructor
	ListNode(T e, Posi(T) p = NULL, Posi(T) s = NULL):data(e),pred(p),succ(s) {}// constructor
	Posi(T) insertAsPred(T const& e);
	Posi(T) insertAsSucc(T const& e);
#include "listNode.h"
tempalte <typename T> class List{
private:	int _size;
			posi(T) header;
			posi(T) trailer;
protected:
public:
};

等效地,在列表中头(header)、首(first)、末(last)、尾(trailer) 节点的秩可分别理解为-1, 0, n-1, n。

构造:

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

无序列表

循秩访问

template <typename T>
T List<T>::operator[](Rank r) const{
	Posi(T) p = first();
	while(0 < r--) p = p -> succ;
	return p->data;
}

取决于列表长度
平均复杂度O(n)
效率过于低下

基于复制的构造

template <typename T>
void List<T>::copyNodes(Posi(T) p, int n){
	init();
	while(n--){
		insertAsLast(p->data);
		p = p->succ;
	}
}

删除和析构

template <typename T>
T List<T>::remove(Posi(T) p){
	T e = p->data;
	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;
}

唯一化

template <typename T> int List<T>::duplicate(){
	if(_size < 2) return 0;
	int oldSize = _size;
	Posi(T) p = first(); Rank r = 1;
	while( trailer != (p = p->succ)){
		Posi(T) q = find(p->data, r, p);
		q? remove(q):r++;
	}
	return oldSize - _size; //被删除原总数
}

有序列表

唯一化

template <typename T> int List<T>::uniquify(){
	if(_size < 2) return 0;
	int oldSize = _size;
	ListNodePosi(T) p = first();
	ListNodePosi(T) q;
	while(trailer != (q=p->succ)){
		if(p->data ! q->data) p = q;
		else remove(q);
	}
	return oldSize-_size;
}

只需遍历列表一次,复杂度O(n)

查找

最好O(1),最坏O(n),等概率事平均O(n),正比于区间宽度
列表不能使用二分查找使时间复杂度降为O(log2n),因为列表不能高效地循秩访问

选择排序

实现

selectionsort
在列表中查找最大值并存放到最前端,每一次都只做一次移动,与起泡排序相比效率有很大提高

//对列表中起始于位置p的连续n个元素做选择排序
template <typename T> void List<T>::selectionSort(Posi(T) p, int n){
	Posi(T) head = p->pred;
	Posi(T) tail = p;
	for (int i = 0; i < n; i++) tail = tail->succ; //[p,p+n)
	while ( n > 1){
		insertBefore(tail,remove(selectMax(head->succ,n)));
		tail = tail->pred;
		n--;
	}
}

性能

总共迭代n次,在第k次迭代中
selectMax()为 θ \theta θ(n-k)
remove() 和 insertBefore()均为O(1)
总体复杂度为 θ ( n 2 ) \theta (n^2) θ(n2)
尽管如此,元素移动操作远远少于起泡排序

插入排序

构思

将序列视为 sorted L[0,r)和 unsorted L[r,n)
将unsorted 依次插入sorted的合适位置

实现

p总是指向当前需要排序的元素

template <typename T> void List<T>::insertionSort(Posi(T) p, int n){
	for (int r = 0; r < n; r++){
		insertAfter(search(p->data,r,p),p->data);
		p = p->succ; remove(p->pred);
	} //n 次迭代,每次O(r+1)
} // 仅使用O(1)的辅助空间,属于就地算法(in-place algorithm)

性能分析

  • 最好情况:完全有序,只需要1次比较,0次交换 O(n)
  • 最坏情况:所有牌按照倒序排,需要O(k)次比较,1次交换, O ( n 2 ) O(n^2) O(n2)

平均性能

backward analysis
查询哪个元素是L[r]:在r+1个元素中,每个元素都有均等概率
[r+(r-1)+(r-2)+…+2+1+0]/(r+1)+1 = r/2+1
O ( n 2 ) O(n^2) O(n2)
插入排序的平均、最坏时间复杂度均为 O ( n 2 ) O(n^2) O(n2)

逆序对

逆序对:p对应的逆序对有多少个,p就要经过多少次比较。
插入排序的时间复杂度 O ( I + n ) O(I+n) O(I+n)
最坏情况:任何一对元素都是逆序的,因此 I = ( 2 n ) = O ( n 2 ) I=(^n_2) = O(n^2) I=(2n)=O(n2)
最好情况: I = 0 , O ( n ) I=0, O(n) I=0,O(n)
input sensitive

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值