【待完善】【算法】知识点小结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wingrez/article/details/85952420

说明:文章中如有错误,欢迎评论告知!
最后更新时间:2019.01.17(近期不会更新)

本文对《算法(第4版中文版)》中的部分知识点作了简要归纳总结。
之后会持续发布更为详细的知识点总结。敬请期待。


一、算法分析(详细地,参见【算法】算法分析

1、近似(需要保留系数)
2、增长的数量级(不需要保留系数)
3、几种增长的数量级(由小到大)
常数级别:1
对数级别:logN
线性级:N
线性对数级:NlogN
平方级:N²
立方级:N³
指数级:2^N


二、合并查找(union-find)算法

解决动态连通性问题,判断两个结点是否相连。

1、API

public class UF
---------------
UF(int N)                        //以整数标识(0到N-1)初始化N的结点
void union(int p, int q)         //在p和q之间添加一条连接
int find(int p)                  //p所在连通分量的标识符
boolean connected(int p, int q)  //检查p与q是否连通
int count()                      //返回连通分量的数量

2、初步实现

public class UF {
	private int[] id;
	private int count;
	
	public UF(int N) {
		count=N;
		id=new int[N];
		for(int i=0;i<N;i++)
			id[i]=i;
	}
	
	public int count() {
		return count;
	}
	
	public boolean connected(int p, int q) {
		return find(p)==find(q);
	}
	
	public int find(int p)
	public void union(int p, int q)
}

3、实现find和union

①快速查找(quick-find)算法:
使得处于同一个连通分量中的结点的id值相同,即每个连通分量都以一个标识符表示。

	public int find(int p) {
		return id[p];
	}
	
	public void union(int p, int q) {
		int pID=find(p);
		int qID=find(q);
		if(pID==qID) return;
        //遍历整个id[]数组,使得标识符为pID的结点更新为qID
		for(int i=0;i<id.length;i++)
			if(id[i]==pID) id[i]=qID;
		count--;
	}

优点:find操作是线性级别的,只需要访问数组一次。
缺点:union操作需要遍历整个id[]数组,无法处理大型问题。
时间复杂度:find->O(1);union->O(N)。

②快速合并(quick-union)算法:
每个结点的id值代表同一连通分量中的下一个结点编号,以此类推,形成链接,直到根结点(根结点的id值就是根结点编号)。根结点编号即连通分量的标识符。每个连通分量可以看成一棵树,那么id[]数组就形成了一个森林。

	private int find(int p) {
        //由当前结点不断链接,直到找到根节点
		while(p!=id[p]) p=id[p];
		return p;
	}
	
	public void union(int p, int q) {
        //将两棵树连接到一起
		int pRoot=find(p);
		int qRoot=find(q);
		if(pRoot==qRoot) return;
		id[pRoot]=qRoot;
		count--;
	}

优点:union操作是线性级别的。
缺点:存在最坏输入的情况(对有序的整数对连续执行find操作:0-1,0-2,0-3...,0-N-1),使得程序运行时间在平方级别。(详见书P143)
时间复杂度:find->O(树的高度);union->O(数的高度)。

③加权合并查找(weighted quick-union)算法
记录每一棵树的大小并总是将较小的树连接到较大的树上,以解决快速合并算法中最坏输入的情况。

public class WeightedQuickUnionUF {
	private int[] id;
	private int[] sz;
	private int count;

	public WeightedQuickUnionUF(int N) {
		count = N;
		id = new int[N];
		for (int i = 0; i < N; i++) id[i] = i;
		sz = new int[N];
		for (int i = 0; i < N; i++)	sz[i] = i;
	}
	
	public int find(int p) {
		while(p!=id[p]) p=id[p];
		return p;
	}
	
	public void union(int p, int q) {
		int i=find(p);
		int j=find(q);
		if(i==j) return;
		if(sz[i]<sz[j]) { id[i]=j; sz[j]+=sz[i]; }
		else { id[j]=i; sz[i]+=sz[j]; }
		count--;
	}
}

优点:算法的性能提升到了对数级别,可以处理规模较大的问题。
时间复杂度:find->O(logN);union->O(logN)

4、相关命题:
①P141:在quick-find算法中,每次find()调用只需访问数组一次,而归并两个分量的union()操作访问数组的次数在(N+3)到(2N+1)之间。
②P143:quick-union算法中的find()方法访问数组的次数为1加上给定结点所对应的结点深度的两倍。union()和connected()访问数组的次数为两次find()操作(如果union中给定的两个结点分别存在于不同的树中则还需要加1)。
③P146:对于N个结点,加权quick-union算法构造的森林中的任意结点的深度最多为logN。

5、总结
各种union-find算法的性能比较(N个结点,最坏情况下)

  构造函数 find union
quick-union N N 1
quick-find N 树的高度 树的高度
weighted quick-union N logN logN

6、最优算法
路径压缩的加权quick-union算法。

7、练习
熟练掌握图1.5.3和图1.5.5


二、排序(sort)算法

将元素的主键以某种方式排列,通常是按照大小或字母顺序。

1、API

public class Sort
-----------------
public static void sort(Comparable[] a)                   //对a数组中的元素排序
private static boolean less(Comparable v, Comparable w)   //比较元素v和w,若v较小返回true
private static void exch(Comparable[] a, int i, int j)    //交换a[i]和a[j]的值
private static void show(Comparable[] a)                  //在单行中打印数组
public static boolean isSorted(Comparable[] a)            //测试数组元素是否有序

2、初步实现

	private static boolean less(Comparable v, Comparable w) {
		return v.compareTo(w)<0;
	}
	
	private static void exch(Comparable[] a, int i, int j) {
		Comparable t=a[i];
		a[i]=a[j];
		a[j]=t;
	}
	
	private static void show(Comparable[] a) {
		for(int i=0;i<a.length;i++) {
			StdOut.print(a[i]+" ");
		}
		StdOut.println();
	}
	
	public static boolean isSorted(Comparable[] a) {
		for(int i=1;i<a.length;i++) {
			if(less(a[i], a[i-1])) return false;
		}
		return true;
	}
	
	public static void sort(Comparable[] a)

3、实现sort

①选择排序(selection sort)
首先,找到数组中最小的元素,其次,将它和数组的第一个元素交换位置。再次,在剩下的元素中找到最小的元素,将它和数组的第二个元素交换位置。如此往复,直到整个数组有序。

	public static void sort(Comparable[] a) {
		int N = a.length;
		for (int i = 0; i < N; i++) {
			int min = i;
            //在i之后找到最小的元素
			for (int j = i + 1; j < N; j++) {
				if (less(a[j], a[min]))
					min = j;
			}
            //交换a[i]和a[min]
			exch(a, i, min);
		}
	}

是否稳定:不稳定(unstable)
优点:相较于其他排序算法,选择排序进行数据交换的次数是最少的,只需交换N次。
缺点:效率较低,原因在于找最小的元素需要每次都遍历数组进行比较。而且对于已经有序的数组(或主键全部相等的数组)不敏感。
时间复杂度:O(N²)

②插入排序(insertion sorting)
对每个待排序的元素不断地向前移动,直到该元素不再小于前一个元素,即元素插入到了正确的位置。

	public static void sort(Comparable[] a) {
		int N=a.length;
		for(int i=1;i<N;i++) {
			for(int j=i; j>0 && less(a[j],a[j-1]); j--) {
				exch(a,j,j-1);
			}
		}
	}

是否稳定:稳定(stable)
优点: 对已经有序的数组敏感。能快速对部分有序的数组进行排序。
缺点:算法效率仍然依赖于输入的有序情况。存在最坏输入的情况,即数组逆序,此时需要~N²/2次比较和~N²/2次交换。
时间复杂度:O(N²)

③归并排序(merge sort)
采用分治的思想,递归地将数组划分成两个子数组,对两个子数组进行排序,排序完再将两个子数组归并以使整个数组有序。比较元素的工作是在merge操作中完成的。merge将两个子数组中的元素从小到大排列合并成一个数组。

	private static Comparable[] aux; //辅助数组,一次性分配N个空间大小

	public static void merge(Comparable[] a, int lo, int mid, int hi) {//将a[lo..mid]和a[mid+1..hi]归并
		for(int k=lo;k<=hi;k++) {//将a[lo..hi]复制到aux[lo..hi]
			aux[k]=a[k];
		}
		
		int i=lo, j=mid+1;
		for(int k=lo;k<=hi;k++) {//归并回到a[lo..hi]
			if(i>mid) a[k]=aux[j++];
			else if(j>hi) a[k]=aux[i++];
			else if(less(aux[j], aux[i])) a[k]=aux[j++];
			else a[k]=aux[i++];
		}
	}

是否稳定:稳定
优点:整个算法的性能在线性对数级。
缺点:不是原地排序(原地排序即所需的额外空间大小≤clogN,c是常数)。归并排序所需的额外空间和N成正比。
时间复杂度:O(NlogN)

划分数组有两种方式,由此产生了2种归并排序。
<1>自顶向下的归并排序

	private static void sort(Comparable[] a, int lo, int hi) {//对数组a[lo..hi]排序
		if(hi<=lo) return;
		int mid=lo+(hi-lo)/2;
		sort(a, lo, mid); 	//对左半边排序
		sort(a, mid+1, hi); //对右半边排序
		merge(a,lo,mid,hi);
	}

可以把整个过程想象成一颗树,根结点是[0..N-1],由根节点对半分成两个孩子,左孩子[0..N/2],右孩子[N/2+1..N-1],以此类推。

<2>自底向上的归并排序

	public static void sort(Comparable[] a) {
		int N=a.length;
		for(int sz=1;sz<N;sz=sz+sz)//sz子数组的大小
			for(int lo=0;lo<N-sz;lo+=sz+sz)//lo:子数组索引
				merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
	}

取代递归,采用迭代的方式,首先把每个元素想象成有1个元素的数组,将这些数组11合并。成为了拥有2个元素的数组,再将这些数组22合并,成为了拥有4个元素的数组。依次类推。

④快速排序(quick sort)
采用分治的思想,递归地将数组划分成两个子数组,对两个子数组进行排序,排序完则整个数组有序。具体地,在数组中选择一个切分元素(通常取第一个元素),使得比切分元素小(≤)的元素都在切分元素的左边,比切分元素大(≥)的元素都在切分元素的右边。

	public static void sort(Comparable[] a) {
		StdRandom.shuffle(a);	//对数组混洗,消除对输入的依赖
		sort(a,0,a.length-1);
	}
	
	private static void sort(Comparable[] a, int lo, int hi) {
		if(hi<=lo) return;
		int j=paratiton(a, lo, hi);
		sort(a, lo, j-1);
		sort(a, j+1, hi);
	}
	
	private static int partition(Comparable[] a, int lo, int hi) {//将数组切分为a[lo..i-1], a[i], a[i+1..hi]
		int i=lo, j=hi+1;	//左右扫描指针
		Comparable v=a[lo];	//切分元素
		while(true) {	//扫描左右,检查扫描是否结束并交换元素
			while(less(a[++i], v)) if(i==hi) break;
			while(less(v, a[--j])) if(j==lo) break;
			if(i>=j) break;
			exch(a, i, j);
		}
		exch(a, lo, j); //将v=a[j]放入正确的位置
		return j;
	}

是否稳定:不稳定
优点:实现简单,适用于各种不同输入数据且在一般应用中比其他排序算法都要快。是原地排序。
缺点:非常脆弱,算法实现容易出现错误,以致造成平方级别的性能。
时间复杂度:O(NlogN),最坏情况下O(N²)
(此处待完善)

4、优先队列(priority queue)
一种数据结构,支持取出队首元素、删除最大或最小元素、插入元素等操作,一般,元素按照降序或升序排列。
考虑元素的存储方式不同,实现的方法不同。有3种存储方式:有序数组、无序数组、堆。
此处考虑最大优先队列,API如下:

public class MaxPQ<Key extends Comparable<Key>>
-----------------------------------------------
MaxPQ(Key[] a)        用a[]中的元素创建一个优先队列
void insert(Key v)    向优先队列中插入一个元素
Key max()             返回最大元素
Key delMax()          删除并返回最大元素
boolean isEmpty()     返回队列是否为空
int size()            返回优先队列中的元素个数

<1>无序数组实现
数组中的元素是无序的。
插入操作:直接将元素插入到数组尾部
删除最大元素操作: 遍历数组找到最大元素,将它和尾部元素交换然后删除。

<2>有序数组实现
数组中的元素是有序的。
插入操作:遍历数组,将所有比插入元素大的元素向后移动一个位置,再将插入元素放在这个空的位置。
删除最大元素操作:删除末尾元素。

<3>堆实现(二叉堆)

public class MaxPQ<Key extends Comparable<Key>>{
	
	private Key[] pq;
	private int N=0;
	
	public MaxPQ(int maxN) {
		pq=(Key[]) new Comparable[maxN+1];
	}
	
	public boolean isEmpty() {//返回队列是否为空
		return N==0;
	}
	
	public void insert(Key v) {//插入一个元素
		pq[++N]=v;
		swim(N);
	}
	
	public Key delMax() {//删除并返回最大元素
		Key max=pq[1];//从根节点得到最大元素
		exch(1, N--);//将其和最后一个结点交换
		pq[N+1]=null;//防止对象游离
		sink(1);
		return max;
	}
	
	private boolean less(int i, int j) {//是否优先队列中i位置的元素小于j位置的元素
		return pq[i].compareTo(pq[j])<0;
	}
	
	private void exch(int i,int j) {//交换优先队列中i位置和j位置的元素
		Key t=pq[i];
		pq[i]=pq[j];
		pq[j]=t;
	}
	
	private void swim(int k) {
		while(k>1 && less(k/2,k)) {
			exch(k/2, k);
			k=k/2;
		}
	}
	
	private void sink(int k) {
		while(2*k<=N) {
			int j=2*k;
			if(j<N && less(j,j+1)) j++;
			if(!less(k,j)) break;
			exch(k,j);
			k=j;
		}
	}
	
}

二叉堆的内部使用数组pq[]实现,该数组可以形成一棵二叉树,pq[1]是根结点。对于父亲结点pq[i],其孩子结点分别是pq[2*i],pq[2*i+1],要求孩子结点的键值小于等于父亲节点的键值。同样,pq[j]的父亲结点是pq[j/2](如果有父亲结点的话)。
插入操作:将元素加到数组末尾(成为树中最后一个结点),对元素进行上浮操作,即不断地比较该元素和其父亲结点,如果该元素大于其父亲结点,则交换二者,直到小于其父亲结点或者该元素到达了根节点。
删除最大元素操作:根节点元素即最大值。将其与最后一个结点交换,并删除它。然后对当前的根节点执行下沉操作,即不断地让该元素和其两个孩子结点中较大的一个比较,如果该元素较小,则交换二者,直到该元素较大或没有孩子结点。(注意:在删除最大元素后,增加了一个空结点,防止对象游离,也有利于比较)。

5、堆排序(heap sort)
采用二叉堆实现优先队列的思想。首先对数组元素构造出一个最大堆,然后对不断地进行删除最大元素的操作。排序是在删除最大元素的过程中实现的,当删除最大元素后,实际把它放在了当前数组的末尾,也就是堆缩小后数组空出的位置。

    public static void sort(Comparable[] a) {   
        int N=a.length;
        for(int k=N/2;k>=1;k--)
            sink(a, k, N);
        while(N>1) {
            exch(a,1,N--);
            sink(a,1,N);
        }
    }

    private static void sink(Comparable[] pq, int k, int n) {
        while (2*k <= n) {
            int j = 2*k;
            if (j < n && less(pq, j, j+1)) j++;
            if (!less(pq, k, j)) break;
            exch(pq, k, j);
            k = j;
        }
    }

是否稳定:不稳定
优点:无需额外空间,是原地排序。对输入并不太依赖,算法较“稳定”。
缺点:实现相对复杂一点,堆的维护较麻烦。
时间复杂度:O(NlogN)

6、总结
几种排序算法的比较

算法 时间复杂度 是否稳定 是否为原地排序
选择排序 O(N²)
插入排序 O(N²)
归并排序 O(NlogN)
快速排序 O(NlogN)
堆排序 O(NlogN)

7、索引优先队列

8、相关命题
①P156:对于长度为N的数组,选择排序需要大约N²/2次比较和N次交换。
②P157:对于随机排列的长度为N且主键不重复的数组,平均情况下插入排序需要~N²/4比较以及~N²/4次交换。最坏情况下需要~N²/2次比较和~N²/2次交换,最好情况下需要N-1次比较和0次交换。
③P158:插入排序需要的交换操作和数组中倒置的数量相同,需要的比较次数大于等于倒置的数量,小于等于倒置的数量加上数组的大小再减一。
④P173:对于长度为N的任意数组,自顶向下的归并排序需要(1/2)NlogN至NlogN次比较。
⑤P173:对于长度为N的任意数组,自顶向下的归并排序最多需要访问数组6NlogN次。
⑥P176:对于长度为N的任意数组,自底向上的归并排序需要(1/2)NlogN至NlogN次比较,最多访问数组6NlogN次。
⑦P177:没有任何基于比较的算法能够保证使用少于lg(N!)~NlogN次比较将长度为N的数组排序。
⑧P178:归并排序是一种渐进最优的基于比较排序的算法。
⑨P186:将长度为N的无重复数组排序,快速排序平均需要~2NlnN(以及1/6的交换)。(2NlnN≈1.39NlogN)(这个不算一下吗?)
⑩P186:快速排序最多需要约N²/2次比较,但随机打乱数组能够预防这种情况。
①P199:根节点是堆有序的二叉树中的最大结点。
②P199:一棵大小为N的完全二叉树的高度为(int)lgN
③P202:对于一个含有N的元素的基于堆的优先队列,插入元素操作只需不超过(logN+1)次比较,删除最大元素的操作需要不超过2logN次比较。
④P206:用下沉操作由N个元素构造堆只需少于2N次比较以及少于N次交换。
⑤P208:将N个元素排序,堆排序只需少于(2NlogN+2N)次比较(以及一办次数的交换)。
 


三、查找算法

1、符号表(symbol table)数据结构
用于将一个键(Key)和一个值(Value)联系起来。
API如下:

public class ST<Key, Value>
---------------------------
ST()                                创建一张符号表
void put(Key key, Value val)        将键值对存入表中,若值为空,则从表中删除key,若键已存在,则更新值
Value get(Key key)                  获取键key对应的值,不存在则返回null
void delete(Key key)                从表中删除key对应的键值对
boolean contains(Key key)           键key是否在表中存在
boolean isEmpty()                   表是否为空
int size()                          返回表中的键值对数量
Iterable<Key> keys()                表中的所有键的集合

2、有序符号表数据结构
符号表中键值对按照键的大小顺序存储。
API如下:

public class ST<Key extends Comparable<key>, Value>
---------------------------
ST()                                创建一张有序符号表
void put(Key key, Value val)        将键值对存入表中,若值为空,则从表中删除key,若键已存在,则更新值
Value get(Key key)                  获取键key对应的值,不存在则返回null
void delete(Key key)                从表中删除key对应的键值对
boolean contains(Key key)           键key是否在表中存在
boolean isEmpty()                   表是否为空
int size()                          返回表中的键值对数量
Key min()                           返回最小的键
Key max()                           返回最大的键
Key floor(Key key)                  小于等于Key的最大键
Key ceiling(Key key)                大于等于Key的最小键
int rank(Key key)                   小于key的键的数量
Key select(int k)                   排名为k的键(排名从0开始)
void deleteMin()                    删除最小的键
void deleteMax()                    删除最大的键
int size(Key lo, Key hi)            [lo..hi]之间键的数量
Iterable<Key> keys(Key lo, Key hi)  [lo..hi]之间的所有键,已排序
Iterable<Key> keys()                表中的所有键的集合,已排序

3、初步实现有序符号表

void delete(Key key){
    put(key, null);
}

boolean contains(Key key){
    return get(key)!=null;
}

boolean isEmpty(){
    return size()==0;
}

void deleteMin(){
    delete(min());
}

void deleteMax(){
    delete(max());
}

int size(Key lo, Key hi){
    if(hi.compareTo(lo)<0)
        return 0;
    else if(contains(hi))
        return rank(hi)-rank(lo)+1;
    else
        return rank(hi)-rank(lo);
}

Iterable<Key> keys(){
    return keys(min(), max());
}

4、具体实现有序符号表
2种方法:基于无序链表的顺序查找、基于有序数组的二分查找

<1>基于无序链表的顺序查找

public class SequentialSerachST<Key, Value> {
	private Node first;//链表首结点
	private class Node {//链表结点的定义
		Key key;
		Value val;
		Node next;
		public Node(Key key, Value val, Node next) {
			this.key=key;
			this.val=val;
			this.next=next;
		}
	}
	
	public Value get(Key key) {
		for(Node x=first; x!=null; x=x.next)
			if(key.equals(x.key))
				return x.val;
		return null;
	}
	
	public void put(Key key, Value val) {
		for(Node x=first;x!=null;x=x.next)
			if(key.equals(x.key)) {//命中,更新
				x.val=val;
				return;
			}
		first=new Node(key,val,first);//未命中,新建结点
	}
}

<2>基于有序数组的二分查找

public class BinarySearchST<Key extends Comparable<Key>, Value> {

	private Key[] keys;
	private Value[] vals;
	private int N;

	public BinarySearchST(int capacity) {
		keys = (Key[]) new Comparable[capacity];
		vals = (Value[]) new Object[capacity];
	}

	public int size() {
		return N;
	}

	public boolean isEmpty() {
		return size() == 0;
	}

	public Value get(Key key) {
		if (isEmpty())
			return null;
		int i = rank(key);
		if (i < N && keys[i].compareTo(key) == 0)
			return vals[i];
		else
			return null;
	}

	public int rank(Key key) {
		int lo = 0, hi = N - 1;
		while (lo <= hi) {
			int mid = lo + (hi - lo) / 2;
			int cmp = key.compareTo(keys[mid]);
			if (cmp < 0)
				hi = mid - 1;
			else if (cmp > 0)
				lo = mid + 1;
			else
				return mid;
		}
		return lo;
	}

	public void put(Key key, Value val) {
		int i = rank(key);
		if (i < N && keys[i].compareTo(key) == 0) {
			vals[i] = val;
			return;
		}
		for (int j = N; j > i; j--) {
			keys[j] = keys[j - 1];
			vals[j] = vals[j - 1];
		}
		keys[i] = key;
		vals[i] = val;
		N++;
	}

	public Key min() {
		return keys[0];
	}

	public Key max() {
		return keys[N - 1];
	}

	public Key select(int k) {
		return keys[N];
	}

	public Key ceiling(Key key) {
		int i = rank(key);
		if (i == N)
			return null;
		return keys[i];
	}

	public Key floor(Key key) {
		int i = rank(key);
		if (i < N && key.compareTo(keys[i]) == 0)
			return keys[i];
		if (i == 0)
			return null;
		else
			return keys[i - 1];
	}

	public void delete(Key key) {
		if (isEmpty())
			return;
		int i = rank(key);
		if (i == N || keys[i].compareTo(key) != 0) {
			return;
		}
		for (int j = i; j < N - 1; j++) {
			keys[j] = keys[j + 1];
			vals[j] = vals[j + 1];
		}
		N--;
		keys[N] = null;
		vals[N] = null;
	}

	public Iterable<Key> keys(Key lo, Key hi) {
		Queue<Key> queue = new Queue<Key>();
		if (lo.compareTo(hi) > 0)
			return queue;
		for (int i = rank(lo); i < rank(hi); i++)
			queue.enqueue(keys[i]);
		if (contains(hi))
			queue.enqueue(keys[rank(hi)]);
		return queue;
	}

}

(此处待完善)

5、二叉查找树(Binary Search Tree,BST)

二叉查找树结合链表和有序数组,是一种更为高效实现有序符号表的数据结构。在二叉查找树中,每个结点只能有一个父结点(根结点除外),每个结点都只有左右两个链接(链接可以为空)。每个结点还包含了一个键和一个值,判断结点的大小是通过比较键实现的(键不重复)。此外,二叉查找树有一个重要的性质,即二叉查找树中每个父亲结点的左孩子和右孩子分别都是一棵二叉查找树,且左孩子及其表示的二叉树所有结点的键都小于该父亲的键,右孩子及其表示的二叉树所有结点的键都大于该父亲的键。

实现如下:

public class BST <Key extends Comparable<Key>, Value> {
	
	private Node root;
	
	private class Node{
		private Key key;
		private Value val;
		private Node left, right;
		private int N;
		
		public Node(Key key, Value val, int N) {
			this.key=key;
			this.val=val;
			this.N=N;
		}
		
	}
	public int size() {
		return size(root);
	}
	
	private int size(Node x) {
		if(x==null) return 0;
		else return x.N;
	}
	
	public Value get(Key key) {
		return get(root, key);
	}
	
	private Value get(Node x, Key key) {
		if(x==null) return null;
		int cmp=key.compareTo(x.key);
		if(cmp<0) return get(x.left, key);
		else if(cmp>0) return get(x.right, key);
		else return x.val;
	}
	
	public void put(Key key, Value val){
		root=put(root, key, val);
	}
	
	private Node put(Node x, Key key, Value val) {
		if(x==null) return new Node(key, val, 1);
		int cmp=key.compareTo(x.key);
		if(cmp<0) x.left=put(x.left, key, val);
		else if(cmp>0) x.right=put(x.right, key, val);
		else x.val=val;
		x.N=size(x.left)+size(x.right)+1;
		return x;
	}
	
}

查找(get):如果树是空的,则查找未命中;如果被查找的键和根节点的键相等,查找命中,否则我们就递归地在适当的子树中继续查找。如果被查找的键较小就选择左子树,较大则选择右子树。
插入(put):如果树是空的,就返回一个含有该键值对的新结点;如果该键等于根结点的键,则更新根结点的值并退出,否则递归地在适当的树中继续进行该过程。如果要插入的键较小就选择左子树,较大则选择右子树。
最小键(min):如果根结点的左链接为空,那么最小键就是根结点;如果不为空,那么最小键就是左子树中的最小键。
最大键(max):如果根结点的右链接为空,那么最大键就是根结点;如果不为空,那么最大键就是右子树中的最大键。
向下取整(floor):如果给定的键key小于根结点的键,那么floor(key)一定在根结点的左子树中;如果给定的键key大于根结点的键,那么只有当根结点右子树中存在小于等于key的结点时,floor(key)才会出现在右子树中,否则根结点就是floor(key)。
向上取整(ceiling):如果给定的键key大于根结点的键,那么ceiling(key)一定在根结点的右子树中;如果给定的键key小于根结点的键,那么只有当根结点左子树中存在大于等于key的结点时,ceiling(key)才会出现在左子树中,否则根节点就是ceiling(key)。
选择(select):查找排名为k的键(即树中正好有k个小于它的键)。如果左子树中的结点数t大于k,那么我们就继续递归地在左子树中查找排名为k的键;如果t等于k,我们就返回根结点中的键;如果t小于k,我们就递归地在右子树中查找排名为(k-t-1)的键。
排名(rank):如果给定的键和根结点的键相等,我们就返回左子树中的结点总数;如果给定的键小于根结点,我们返回该键在左子树中的排名(递归计算);如果给定的键大于根结点,我们返回t+1+它在右子树中的排名(递归计算)。
删除最小键(deleteMin):不断深入根结点的左子树中直至遇见一个空链接,然后将指向该结点的链接指向该结点的右子树,并返回该链接。
删除最大键(deleteMax):不断深入根结点的右子树中直至遇见一个空链接,然后将指向该结点的链接指向该结点的左子树,并返回该链接。
删除(delete):删除任意只有一个子结点(或没有子结点)的结点的步骤同删除最大键或删除最小键。删除一个拥有两个子结点的结点x,在删除结点x后用它的后继结点(即其右子树中的最小结点)填补它的位置。具体步骤如下:将指向即将被删除的结点的链接保存为t;将x指向它的后继结点min(t.right);将x的右链接指向deleteMin(t.right);将x的左链接(空)设为t.left。

(此处待完善)

(点击查看大图)

(1.get)(2.put)(3.floor)(4.select)

(理解书中图3.2.4、图3.2.5、3.2.11)


四、图(最小生成树、最短路径)

前述:

加权边数据结构

public class Edge implements Comparable<Edge> { 

    private final int v;
    private final int w;
    private final double weight;

    public Edge(int v, int w, double weight) {
        this.v = v;
        this.w = w;
        this.weight = weight;
    }

    public double weight() {
        return weight;
    }
    
    public int either() {
        return v;
    }

   
    public int other(int vertex) {
        if      (vertex == v) return w;
        else if (vertex == w) return v;
        else throw new IllegalArgumentException("Illegal endpoint");
    }

    public int compareTo(Edge that) {
        if(this.weight()<that.weight()) return -1;
        else if(this.weight()>that.weight()) return 1;
        else return 0;
    }

    public String toString() {
        return String.format("%d-%d %.5f", v, w, weight);
    }
    
}

加权无向图数据结构

public class EdgeWeightedGraph {

	private final int V;
	private int E;
	private Bag<Edge>[] adj;
	
	public EdgeWeightedGraph(int V) {
		this.V=V;
		this.E=0;
		adj=(Bag<Edge>[]) new Bag[V];
		for(int v=0;v<V;v++)
			adj[v]=new Bag<Edge>();
	}
	
	public int V() { return V; }
	public int E() { return E; } 
	
	public void addEdge(Edge e) {
		int v=e.either(), w=e.other(v);
		adj[v].add(e);
		adj[w].add(e);
		E++;
	}
	
	public Iterable<Edge> adj(int v){
		return adj[v];
	}
	
	public Iterable<Edge> edges(){
		Bag<Edge> b=new Bag<Edge>();
		for(int v=0;v<V;v++)
			for(Edge e: adj[v])
				if(e.other(v)>v) b.add(e);
		return b;
	}
}

加权有向图数据结构 

public class EdgeWeightedDigraph {
	
	private final int V;
	private int E;
	private Bag<DirectedEdge>[] adj;
	
	public EdgeWeightedDigraph(int V) {
		this.V=V;
		this.E=0;
		adj=(Bag<DirectedEdge>[]) new Bag[V];
		for(int v=0;v<V;v++) 
			adj[v]=new Bag<DirectedEdge>();
	}
	
	public int V() { return V; }
	public int E() { return E; }
	
	public void addEdge(DirectedEdge e) {
		adj[e.form()].add(e);
		E++;
	}
	
	public Iterable<DirectedEdge> adj(int v){
		return adj[v];
	}
	
	public Iterable<DirectedEdge> edges(){
		Bag<DirectedEdge> bag=new Bag<DirectedEdge>();
		for(int v=0;v<V;v++)
			for(DirectedEdge e: adj[v])
				bag.add(e);
		return bag;
	}
	
}

切分定理:
图的一种切分是将图的所有顶点分成两个非空且不重叠的两个集合。横切边是一条连接两个属于不同集合的顶点的边。在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图的最小生成树。

1、最小生成树(Minimum Spanning Tree,MST)

图的最小生成树是一棵含有其所有顶点的无环连通子图。加权图的最小生成树是它的一棵权值和最小的生成树。求解最小生成树问题,通常都是利用贪心算法,由此诞生两种算法,Prim算法(2种版本)和Kruskal算法。

<1>Prim算法的延时实现

Prim算法中,每一步都会为一棵生长中的树添加一条边。一开始这棵树只有一个定点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中。有一点需要注意,连接新加入树中的顶点与其他已经在树中顶点的所有边都失效了(这样的边已经不是横切变,因为它的两个顶点都在树中了),所以我们需要从优先队列中删除它们。删除有两种方式,由此产生Prim的两种版本:
即时:直接将这些边从优先队列中删除;
延时:将这些边先留在优先队列中,等到要删除它们的时候再检查边的有效性。

public class LazyPrimMST {

	private double weight;//最小生成树的权值和
	private boolean[] marked;//如果顶点v在树中,那么marked[v]=true
	private Queue<Edge> mst;//最小生成树的边
	private MinPQ<Edge> pq;//使用最小优先队列存储横切边
	
	public LazyPrimMST(EdgeWeightedGraph G) {
		pq=new MinPQ<Edge>();
		marked=new boolean[G.V()];
		mst=new Queue<Edge>();
		
		visit(G, 0);
		while(!pq.isEmpty()) {
			Edge e=pq.delMin();
			int v=e.either(), w=e.other(v);
			if(marked[v] && marked[w]) continue;//跳过失效的边
			mst.enqueue(e);
			weight += e.weight();
			if(!marked[v]) visit(G, v);//将顶点v或w添加到树中
			if(!marked[w]) visit(G, w);
		}
	}
	
	private void visit(EdgeWeightedGraph G, int v) {//标记顶点v并将所有连接v和未被标记顶点的边加入pq
		marked[v]=true;
		for(Edge e: G.adj(v))
			if(!marked[e.other(v)])
				pq.insert(e);
	}
	
	public Iterable<Edge> edges(){
		return mst;
	}
	
	public double weight() {
		return weight;
	}
	
}

(延时实现的Prime算法轨迹)
(理解书中图4.3.10)

<2>Prim算法的即时实现

只在优先队列中保存每个非树顶点w的一条边:将它与树中的顶点连接起来的权重最小的那条边(将w和树的顶点连接起来的其他权重较大的边迟早都会失效,所以没必要在优先队列中保存它们)。Prim算法会从优先队列中取出一个顶点v并检查它的邻接链表中的每条边v-m。如果w已经被标记过,那么这条边就已经失效了;如果w不在优先队列中,或者v-w的权重小于目前已知的最小值edgeTo[w],那么更新数组,将v-m作为将w和树连接的最佳选择。

public class PrimeMST {
	
	private Edge[] edgeTo;//edge[w],顶点w距离树最近的边
	private double[] distTo;//dist[w]=edgeTo[w].weight()
	private boolean[] marked;//如果v在树中,那么marked[v]=true
	private IndexMinPQ<Double> pq;//有效的横切边
	
	public PrimMST(EdgeWeightedGraph G) {
		edgeTo=new Edge[G.V()];
		distTo=new double[G.V()];
		marked=new boolean[G.V()];
		for(int v=0;v<G.V();v++)
			distTo[v]=Double.POSITIVE_INFINITYl;
		pq=new IndexMinPQ<Double>(G.V());
		
		distTo[0]=0.0;
		pq.insert(0, 0.0);
		while(!pq.isEmpty())
			visit(G, pq.delMin());
	}
	
	private void visit(EdgeWeightedGraph G, int v) {
		marked[v]=true;
		for(Edge e: G.adj(v)) {
			int w=e.other(v);
			if(marked[w]) continue;
			if(e.weight()<distTo[w]) {
				edgeTo[w]=e;
				distTo[w]=e.weight();
				if(pq.contains(w)) pq.change(w, distTo[w]);
				else pq.insert(w,distTo[w]);
			}
		}
	}
	
	public Iterable<Edge> edges()
	public double weight()
	
}

(即时实现的Prim算法轨迹)
(理解书中图4.3.12)

两种版本的加边顺序是相同的。

<3>Kruskal算法

按照边的从小到大的权重顺序,依次将边加入最小生成树中,保证加入的边不会与已经加入的边构成环,直到树中含有V-1条边为止。与Prim算法是一条边一条边地来构造最小生成树,每一步都为一棵树添加一条边。Kruskal构造最小生成树的时候也是一条边一条边地构造,但不同的是它寻找的边会连接一片森林中的两棵树。我们从一片由V棵单顶点的树构成的森林开始,用可以找到的最短边,不断地将两棵树合并,直到只剩下一棵树,它就是最小生成树。

public class KruskalMST {
	
	private Queue<Edge> mst;
	
	public KruskalMST(EdgeWeightedGraph G) {
		mst=new Queue<Edge>();
		MinPQ<Edge> pq=new MinPQ<Edge>();
		for(Edge e: G.edges()) pq.insert(e);
		UF uf=new UF(G.V());
		while(!pq.isEmpty() && mst.size()<G.V()-1) {
			Edge e=pq.delMin();//从pq得到权重最小的边和它的顶点
			int v=e.either(), w=e.other(v);
			if(uf.connected(v, w)) continue;//忽略失效的边
			uf.union(v, w);//合并分量
			mst.enqueue(e);//将边添加到最小生成树中
		}
	}
	
	public Iterable<Edge> edges(){
		return mst;
	}
	
	public double weight()
	
}

(Kruskal算法轨迹)
(理解书中图4.3.14)

两种算法的比较

算法 加边策略 数据结构 存储的内容 性能
Prim(lazy) 加入与T邻接的最小边 T与其补集 PQ T的邻接边和失效边 ElogE
prim(eager) 加入与T邻接的最小边 T与其补集 索引PQ 与T邻接的顶点v、v连向T的最小边的权值 ElogE
Kruskal 加入使T不形成环的最小边 T中某一连通分量及其补集 索引PQ G中的边 ElogE

2、最短路径(shortest path)

<1>Dijkstra算法

Dijkstra算法适用于边权非负的加权有向图的单起点最短路径问题。它首先将distTo[s]初始化为0,distTo[]中的其他元素初始化为正无穷。然后将distTo[]最小的非树顶点松弛并加入树中,如此这般,直到所有的顶点都在树中或者所有的非树顶点的distTo[]值均为无穷大。

public class DijkstraSP {
	
	private DirectedEdge[] edgeTo;
	private double[] distTo;
	private IndexMinPQ<Double> pq;

	public DijkstraSP(EdgeWeightedDigraph G, int s) {
		edgeTo = new DirectedEdge[G.V()];
		distTo = new double[G.V()];
		pq = new IndexMinPQ<Double>(G.V());
		for (int v = 0; v < G.V(); v++)
			distTo[v] = Double.POSITIVE_INFINITY;
		distTo[s] = 0.0;
		pq.insert(s, 0.0);
		while (!pq.isEmpty())
			relax(G, pq.delMin());
	}

	private void relax(EdgeWeightedDigraph G, int v) {
		for (DirectedEdge e : G.adj(v)) {
			int w = e.to();
			if (distTo[w] > distTo[v] + e.weight()) {
				distTo[w] = distTo[v] + e.weight();
				edgeTo[w] = e;
				if (pq.contains(w))
					pq.change(w, distTo[w]);
				else
					pq.insert(w, distTo[w]);
			}
		}
	}

	public double distTo(int v) {
		return distTo[v];
	}

	public boolean hasPathTo(int v) {
		return distTo[v] < Double.POSITIVE_INFINITY;
	}

	public Iterable<DirectedEdge> pathTo(int v) {
		if (!hasPathTo(v))
			return null;
		Stack<DirectedEdge> path = new Stack<DirectedEdge>();
		for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
			path.push(e);
		}
		return path;
	}

}

<2>Bellman-Ford算法


五、字符串

1、字母表数据结构
其API如下:

public class Alphabet
---------------------
Alphabet(String s)              根据s中的字符创建一张新的字母表
char toChar(int index)          获取字母表中索引为index的字符
int toIndex(char c)             获取c的索引,在0~R-1之间
boolean contains(char c)        判断c是否在字母表中
int R()                         基数,即字母表中的字符数量
int lgR()                       表示一个索引所需的位数
int[] toIndices(String s)       将s转换为R进制的整数
String toChars(int[] indices)   将R进制的整数转换为基于该字母表的字符串

2、字符串排序
三种字符串排序方法:键索引计数法、低位优先(LSD)的字符串排序、高位优先(MSD)的字符串排序

<1>键索引计数法
适用于按照键的顺序为一组字符串排序。

int N=a.length;
String[] aux=new String[N];
int[] count=new int[R+1];

//计算出现频率
for(int i=0;i<N;i++)
    count[a[i].key()+1]++;
//将频率转换为索引
for(int r=0;r<R;r++)
    count[r+1]+=count[r];
//将元素分类
for(int o=0;i<N;i++)
    aux[count[a[i].key()]++]=a[i];
//回写
for(int i=0;i<N;i++)
    a[i]=aux[i];

<2>LSD
适用于长度都相等的一组字符串排序。假设这些字符串的长度为W,从右向左以每个字符作为键,用键索引计数法对这组字符串排序W遍。

public class LSD {
	public static void sort(String[] a, int W) {
		//通过前W个字符将a[]排序
		int N=a.length;
		int R=256;
		String[] aux=new String[N];
		
		for(int d=W-1;d>=0;d--) {
			//根据第d个字符用键索引计数法排序
			int[] count=new int[R+1];//计算出现频率
			for(int i=0;i<N;i++)
				count[a[i].charAt(d)+1]++;
			for(int r=0;r<R;r++)//将频率转换为索引
				count[r+1]+=count[r];
			for(int i=0;i<N;i++)//将元素分类
				aux[count[a[i].charAt(d)]++]=a[i];
			for(int i=0;i<N;i++)//回写
				a[i]=aux[i];
		}
	}
}

<3>MSD

public class MSD {
	private static int R=256;//基数
	private static final int M=15;//小数组的切换阈值
	private static String[] aux;//数据分类的辅助数组
	
	private static int charAt(String s,int d) {
		if(d<s.length()) return s.charAt(d);
		else return -1;
	}
	
	public static void sort(String[] a) {
		int N=a.length;
		aux=new String[N];
		sort(a,0,N-1,0);
	}
	
	private static void sort(String[] a, int lo, int hi, int d) {
		//以第d个字符为键将a[lo]至a[hi]排序
		if(hi<=lo+M) {
			Insertion.sort(a,lo,hi,d);
			return;
		}
		int[] count=new int[R+2];//计算频率
		for(int i=lo;i<=hi;i++)
			count[charAt(a[i],d)+2]++;
		for(int r=0;r<R+1;r++)//将频率转换为索引
			count[r+1]+=count[r];
		for(int i=lo;i<=hi;i++)//数据分类
			aux[count[charAt(a[i],d)+1]++]=a[i];
		for(int i=lo;i<=hi;i++)//回写
			a[i]=aux[i-lo];
		//递归的以每个字符为键进行排序
		for(int r=0;r<R;r++)
			sort(a,lo+count[r],lo+count[r+1]-1,d+1);
	}
	
}

3、单词查找树
<1>API

public class StringST<Value>
----------------------------
StringST()                                      创建一个符号表
void put(String key, Value val)                 向表中插入键值对,如果值为null,则删除键key
Value get(String key)                           返回键key所对应的值,如果键不存在则返回null
void delete(String key)                         删除键key和它的值
boolean contains(String key)                    判断是否存在key的值
boolean isEmpty()                               判断符号表是否为空
String longestPrefixOf(String s)                返回s的前缀中最长的键
Iterable<String> keyWithPrefix(String s)        返回所有以s为前缀的键
Iterable<String> keysThatMatch(String s)        返回所有和s匹配的键(其中"."能够匹配任意字符)
int size()                                      返回键值对的数量
Iterable<String> keys()                         返回符号表中的所有键

<2>实现

package exam;

import java.util.Queue;

public class TrieST<Value>{
	private static int R=256;//基数
	private Node root;//单词查找树的根结点‘
	
	private static class Node{
		private Object val;
		private Node[] next=new Node[R];
	}
	
	public Value get(String key) {
		Node x=get(root, key, 0);
		if(x==null) return null;
		return (Value)x.val;
	}
	
	private Node get(Node x, String key, int d) {
		//返回以x作为根结点的子单词查找树中与key相关联的值
		if(x==null) return null;
		if(d==key.length()) return x;
		char c=key.charAt(d);//找到第d个字符所对应的子单词查找树
		return get(x.next[c], key, d+1);
	}
	
	public void put(String key, Value val) {
		root=put(root, key, val, 0);
	}
	
	private Node put(Node x, String key, Value val, int d) {
		//如果key存在于以x为根结点的子单词查找树中则更新与它相关联的值
		if(x==null) x=new Node();
		if(d==key.length()) {
			x.val=val;
			return x;
		}
		char c=key.charAt(d);//找到第d个字符所对应的子单词查找树
		x.next[c]=put(x.next[c], key, val, d+1);
		return x;
	}
	
	public Iterable<String> keys(){
		return keysWithPrefix("");
	}
	
	public Iterable<String> keysWithPrefix(String pre){
		Queue<String> q=new Queue<String>();
		collect(get(root, pre, 0), pre, q);
		return q;
	}
	
	private void collect(Node x, String pre, Queue<String> q) {
		if(x==null) return;
		if(x.val!=null) q.enqueue(pre);
		for(char c=0;c<R;c++)
			collect(x.next[c], pre+c, q);
	}
	
	public Iterable<String> keysThatMatch(String pat){
		Queue<String> q=new Queue<String>();
		collect(root, "", pat, q);
		return q;
	}
	
	public void collect(Node x, String pre, String pat, Queue<String> q) {
		int d=pre.length();
		if(x==null) return;
		if(d==pat.length() && x.val!=null) q.enqueue(pre);
		if(d==pat.length()) return;
		
		char next=pat.charAt(d);
		for(char c=0;c<R;c++)
			if(next=='.' || next==c)
				collect(x.next[c], pre+c, pat, q);
	}
	
	public String longestPrefixOf(String s) {
		int length=search(root, s, 0, 0);
		return s.substring(0,  length);
	}
	
	private int search(Node x, String s, int d, int length) {
		if(x==null) return length;
		if(x.val!=null) length=d;
		if(d==s.length()) return length;
		char c=s.charAt(d);
		return search(x.next[c], s, d+1, length);
	}
	
	public void delete(String key) {
		root=delete(root, key, 0);
	}
	
	private Node delete(Node x, String key, int d) {
		if(x==null) return null;
		if(d==key.length()) 
			x.val=null;
		else {
			char c=key.charAt(d);
			x.next[c]=delete(x.next[c], key, d+1);
		}
		if(x.val!=null) return x;
		for(char c=0;c<R;c++)
			if(x.next[c]!=null) return x;
		return null;
	}

}

4、子字符串查找
子字符串查找的API


<1>暴力子字符串查找算法

public static int search(String pat, String txt){
    int M=pat.length();
    int N=txt.length();
    for(int i=0;i<=N-M;i++){
        int j;
        for(j=0;j<M;j++)
            if(txt.charAt(i+j)!=pat.charAt(j))
                break;
        if(j==M) return i;//找到匹配
    }
    return N;//未找到匹配
}

<2>Knuth-Morris-Pratt(KMP)子字符串查找算法

public class KMP {
	private String pat;
	private int[][] dfa;
	
	public KMP(String pat) {
		//由模式字符串构造DFA
		this.pat=pat;
		int M=pat.length();
		int R=256;
		dfa=new int[R][M];
		dfa[pat.charAt(0)][0]=1;
		for(int X=0,j=1;j<M;j++) {
			//计算dfa[][j]
			for(int c=0;c<R;c++)
				dfa[c][j]=dfa[c][X];//复制匹配失败情况下的值
			dfa[pat.charAt(j)][j]=j+1;//设置匹配成功情况下的值
			X=dfa[pat.charAt(j)][X];//更新重启状态
		}
	}
	
	public int search(String txt) {
		//在txt上模拟DFA的运行
		int i, j, N=txt.length(), M=pat.length();
		for(i=0,j=0;i<N && j<M; i++)
			j=dfa[txt.charAt(i)][j];
		if(j==M) return i-M;//找到匹配(到达模式字符串的结尾)
		else return N;//未找到匹配(到达文本字符串的结尾)
	}
	
}

<3>Boyer-Moore字符串查找算法

public class BoyerMoore {
	private int[] right;
	private String pat;
	
	BoyerMoore(String pat){
		//计算跳跃表
		this.pat=pat;
		int M=pat.length();
		int R=256;
		right=new int[R];
		for(int c=0;c<R;c++)
			right[c]=-1;//不包含在模式字符串中的字符的值为-1
		for(int j=0;j<M;j++)//包含在模式字符串中的字符的值为
			right[pat.charAt(j)]=j;//它在其中出现的最右位置
	}
	
	public int search(String txt) {
		//在txt中查找模式字符串
		int N=txt.length();
		int M=pat.length();
		int skip;
		for(int i=0;i<=N-M;i+=skip) {
			//判断模式字符串和文本在位置i是否匹配
			skip=0;
			for(int j=M-1;j>=0;j--) {
				if(pat.charAt(j)!=txt.charAt(i+j)) {
					skip=j-right[txt.charAt(i+j)];
					if(skip<1) skip=1;
					break;
				}
			}
			if(skip==0) return i;//找到匹配
		}
		return N;//未找到匹配
	}
	
}

<4>Rabin-Karp指纹字符串查找算法

public class RabinKarp {
	private String pat;
	private long patHash;
	private int M;
	private long Q;
	private int R=256;
	private long RM;
	
	public RabinKarp(String pat) {
		this.pat=pat;
		this.M=pat.length();
		Q=longRandomPrime();
		RM=1;
		for(int i=1;i<=M-1;i++)
			RM=(R*RM)%Q;
		patHash=hash(pat,M);
	}
	
	public boolean check(int i) {
		return true;
	}
	
	private long hash(String key, int M){
		long h=0;
		for(int j=0;j<M;j++)
			h=(R*h+key.charAt(j))%Q;
		return h;
	}
	
	public int search(String txt) {
		int N=txt.length();
		long txtHash=hash(txt, M);
		if(patHash==txtHash && check(0)) return 0;
		for(int i=M;i<N;i++) {
			txtHash=(txtHash+Q-RM*txt.charAt(i-M)%Q)%Q;
			txtHash=(txtHash*R+txt.charAt(i))%Q;
			if(patHash==txtHash)
				if(check(i-M+1)) return i-M+1;
		}
		return N;
	}
}


 

猜你喜欢

转载自blog.csdn.net/wingrez/article/details/85952420
今日推荐