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)
归并排序
- 无序向量的递归分解
- 有序向量的逐层归并
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