一、排序
1.选择排序(比较次数n2/2,交换次数n,原理是每次都找到最小的元素与第一个元素交换位置)
int[] sort(int[] a){
//用于储存最小值
int b = 0;
int N = a.length;
for(int i=0; i<N; i++){
//将a[i]和a[i+1]~a[N-1]中的最小值交换
int min = i;
//找出最小元素
for(int j=i+1; j<N; j++){
if(a[min]>a[j]){
min = j;
}
}
//交换
b = a[i];
a[i] = a[min];
a[min] = b;
}
return a;
}
2.插入排序(原理就像整理桥牌,将每一张牌插入已经有序的牌中,后面的牌右移。当索引到达最右边时,排序结束。排序时间与输入有关。平均需要n2/4次比较和交换,最差需要n2/2次比较
和交换,最好情况下需要n-1次比较和0次交换)
int[] sort(int[] a){
//遍历
int N = a.length;
//用于储存a[j]
int b = 0;
for(int i=1; i<N; i++){
//从a[i]向前遍历
for(int j=i; j>0&&a[j]<a[j-1]; j--){
//交换a[j]和a[j-1]
b = a[j];
a[j] = a[j-1];
a[j-1] = b;
}
}
}
3.希尔排序(基于插入排序,解决了元素只能一点一点移动的缺点,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序)
希尔排序的思想是使数组中任意间隔为h的元素都是有序的,一个h有序数组就是h个互相独立的有序数组编制在一起形成一个数组。如果h很大,我们就能将元素移到很远的地方。
增幅h的初始值是数组长度乘以一个常数因子,最小为1。希尔排序无法准确描述其性能,只知道最差条件下比较次数是n的3/2次方,比平方性能高。
int[] sort(int[] a){
//数组长度
int N = a.length;
//用于储存a[j]
int b = 0;
//初始化h(数组间隔,子数组数量)
int h = 1;
while(h < N/3){
h = 3h+1;
}
while(h>1){
//处理每个子数组
for(int i=h; i<N; i++){
//对每个子数组进行插入排序
for(int j=i; j>=h&&a[j]<a[j-h]; j-=h){
//交换a[j]和a[j-h]
b = a[j];
a[j] = a[j-h];
a[j-h] = b;
}
}
//缩小h直到1为止
h=h/3;
}
}
4.归并排序(归并,就是将两个有序数组归并成一个大的有序数组。根据这个操作发明了一种递归排序算法:归并排序。
要将一个数组排序,可以先递归地将它分成两半分别排序,再将结果归并起来。
排序所需时间为nlogn,缺点是需要的额外空间和N成正比)
merge方法(下面的两种归并排序都要用到):
//归并a[lo...mid]和a[mid+1...hi]
void merge(Comparable[] a,int lo,int mid,int hi){
//设置遍历起点
int i=lo,j=mid+1;
//将a[lo...hi]复制到aux[lo...hi]
for(int k=lo;k<=hi;k++){
aux[k] = a[k];
}
//将aux[lo...hi]归并到a[lo...hi]
for(int k=lo;k<=hi;k++){
if(i>mid){a[k]=aux[j++];} //左边数组用尽
else if(j>hi){a[k]=aux[i++];} //右边数组用尽
else if(aux[j]<aux[i]){a[k]=aux[j++];} //右边当前元素小,取右边
else {a[k]=aux[i++];} //左边当前元素小,取左边
}
}
4.1自顶向下的归并排序(需要比较1/2nlogn~nlogn次,最多需要访问数组6nlogn次,因为每次归并最多需要访问数组6n次,2n复制,2n移动回去,最多比较2n次)
void sort(Comparable[] a, int lo, int li){
//将数组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,li); //归并左右两个有序数组
}
4.2自底向上的归并排序(进行logn次两两归并)
void sort(Comparable[] a){
//数组长度
int N = a.length;
aux = xxxx;
//子数组的大小每次翻倍
for(int sz=1; sz<N; sz=2sz){
//merge每一个子数组
for(int lo=0; lo<N-sz; lo+=2sz){
merge(a,lo,lo+sz-1;Math.min(lo+2sz-1,N-1));
}
}
}
5.快速排序(优点是实现简单,快时间nlogn,原地排序需要的辅助栈小。其他算法都不能同时提高时间和空间效率。但是缺点是脆弱,不小心会导致性能只有平方级别)
快速排序是一种分治的排序算法。它是将一个数组分成两个子数组,两部分独立进行排序。快速排序和归并排序是互补的:
归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;快速排序是两个子数组有序时整个数组也就自然有序了。
归并排序的递归调用发生在处理数组之前,快速排序的递归调用发生在处理数组之后。
归并排序一个数组被等分成两半,快速排序切分的位置取决于数组的内容。
快速排序需要2nlogn次比较以及1/3nlogn次交换。
快速排序当每次切分后总有一个子数组为空的情况下,比较次数为n2/2,随机打乱数组可以预防。切分不平衡时会导致快速排序的低效,因为每次切分排序的元素太少了。
//打乱数组
StdRandom.shuffle(a);
sort(a,0,a.length-1);
void sort(comparable[] a,lo,hi){
if(hi<=lo){return;}
int j = partition(a,lo,hi);
sort(a,lo,j-1);
sort(a,j+1,hi);
}
//将数组切分成a[lo...i-1],a[i],a[i+1...li],并返回i
int partition(comparable[] a,lo,hi){
//左右扫描指针
int i=lo, j=hi+1;
//切分元素
Comparable v = a[lo];
while(true){
while(a[++i]<v) if(i==hi) break;
while(a[--j]>v) if(j==lo) break;
if(i>=j){break;}
//交换两个数组元素
exch(a,i,j);
}
//将切分元素放入正确的切分位置
exch(a,lo,j);
//返回切分点
return j;
}
6.堆排序(优先队列)
//堆上浮
void swim(int k){
while(k>1 && less(k/2,k)){
exch(k/2,k);
k=k/2;
}
}
//堆下沉
void sink(int k){
while(2*k<=N){
int j = 2*k;
if(j<N && less(j,j+1)){j++;}
if(less(j,k)){break;}
exch(k,j);
k=j;
}
}
//插入数据,N为数组长度-1
void insert(key v){
pq[++N] = v;
swim(N);
}
//删除最大数据
key delMax(){
key max = pq[1];
exch(1,N--);
pq[N+1] = null;
sink(1);
return max;
}
//堆排序
//原始数据
int[] a;
int[] pq = [];
//构造二叉堆。每插入一条数据就上浮一次
for(int N=1;N<a.size;N++){
//先赋值到尾部,跳过a[0]
pq[N] = a[N];
//上浮
swim(N);
}
//从堆顶依次取出所有数据,并给空数组赋值
int[] b = [];
for(int N=1;N<a.size;N++){
b[N-1] = delMax();
}
二、查找
1.二叉查找树及其相关方法
//查找
value getNode(Node x,Key key){
//在以x为根节点的子树中查找并返回key所对应的值
if(x==null){return null;}
int a = key.compareTo(x.key);
if(a<0){return getNode(x.left,key);}
else if(a>0){return getNode(x.left,key);}
else return x.val;
}
//插入
//这个函数的特点是输入的节点和返回的节点都是同一个
Node put(Node x,Key key,Value val){
//在以x为根节点的子树中更新键的值或插入新的节点
if(x==null){
return new Node(key,val,1);
}
int a = key.compareTo(x.key);
if(a<0){x.left=put(x.left,key,val);}
else if(a>0){x.right=put(x.right,key,val);}
else x.val=val;
//不管是更新的value值还是更新的左右链接,都是更新x
return x;
}
//查询最大
同查询最小
//查询最小
Node min(Node root){
if(x.left==null){return left;}
return min(x.left);
}
//查找大于等于key的最小键
同下,只需将左变为右同时将小于变为大于。
//查找小于等于key的最大键(给定的键小于二叉树根节点,那必然在左子树中;若大于根节点,则可能在右子树中或者就是根节点)
//x代表根节点
Node floor(Node x,key){
if(x==null){return null;}
int a = key.compareTo(x.key);
if(a==0){return x;}//等于直接返回
if(a<0){return floor(x.left,key);}
//下面是a>0
Node t = floor(x.right,key);
if(t!=null){return t;}
//返回根节点
else{return x;}
}
//删除最小值是将原来指向删除节点的链接重新指向删除节点的右子树(即使右链接不存在也没问题,不就是最后x.left=null吗):
Node deleteMin(Node x){
if(x.left==null){return x.right;}//删除最小的返回次小的
//有左子树则继续往下递归
x.left = deleteMin(x.left);
return x;
}
删除节点的4个步骤:
1.把将要删除的节点x对象的链接保存为t(也就是另外保存一份)
2.将指向要删除节点x的父链接指向它的后继节点min(t.right)
3.将新的节点x的右链接指向deleteMin(t.right),因为这些节点都大于新的节点x
4.将新的节点x的左链接指向t.left
//删除节点(二叉树中最难)
//x指的是根节点,方法返回值是代替原来根节点的节点
Node delete(Node x,Key key){
//遍历完成,没找到,不删除
if(x==null){return null;}
int a = key.compareTo(x.key);
//删左边
if(a<0) x.left = delete(x.left);
//删右边
if(a>0) x.right = delete(x.right);
//找到要删除的节点,将引用保存到t方便重建代替删除节点的新节点的左右子树
Node t = x;
//用右子树的最小节点代替
x=min(t.right);
//构建新节点的右子树
x.right=deleteMin(t.right);
//构建新节点的左子树
x.left=t.left;
//返回替代已删除节点的新的根节点
return x;
}
2.平衡查找树及其相关方法
2.1红黑二叉查找树(原理是2-3树)
2-3树:把将未命中的查找结束于空链接变成结束于一个2-节点使他变成一个3-节点
2-3树插入算法的根本是变换都是局部的,变更的链接数量不会超过一个很小的常数。这些局部变换不影响树的全局有序性和平衡性。任意空链接到根节点的距离都是相等的。
标准二叉树从上向下生长,有最坏情况下的性能问题;2-3树从下向上生长,没有性能问题。
红黑树既是二叉查找树又是2-3树,结合了二叉树简单的查找算法和2-3树高效的平衡插入算法。
红黑树的定义:红链接均为左连接;没有任何一个节点同时与两个红链接相连;该树是完美黑色平衡的,也就是任意空链接到根节点的路径上的黑链接数量相同。
//左旋转(将红链接从右链接调整到左链接)
Node rotateLeft(Node x){
Node x=h.right;
h.right=x.left;
x.left=h;
x.color=h.color;
h.color=RED;
return x;
}
//判断连接某节点的链接是否为红色
boolean isRed(Node x){
return x.color==RED;
}
//转换链接颜色(最后一步)
void flipColor(Node x){
x.color=RED;
x.left.color=BLACK;
x.right.color=BLACK;
}
//红黑树的插入算法
public void put(Key key,Value value){
//root代表根节点
root = put(root,key, value);
//查找键值,找不到就新建一个
root.color = BLACK;//直接将根节点的链接调整为黑色
}
private Node put(Node h,Key key, Value value){
if (h==null) return new Node(key, value, 1, RED);//注意,插入新节点以后直接返回,不再向下执行
int cmp = key.compareTo(h.key);
if (cmp<0) h.left = put(h.left, key, value);
//递归查找
else if (cmp>0) h.right = put(h.right, key, value);
else h.value = value;//感觉这里可以直接return
//步骤越多越靠上,这样可以少写代码,重复利用代码(顺序是左旋转,右旋转,改变颜色)
if (isRed(h.right) && !isRed(h.left)) h=rotateLeft(h); //新键介于两者之间,将下层的红链接左旋转,下一步再将上层的红链接右旋转,下下步再改变颜色。
if (isRed(h.left) && isRed(h.left.left)) h=rotateRight(h); //新键是最小的,将上层的红链接右旋转,下一步再改变颜色
if (isRed(h.left) && isRed(h.right)) flipColors(h); //新键是最大的,只需要改变颜色即可
h.N = size(h.left)+size(h.right)+1;
//更新走过的节点的N值
return h;
}
向一棵双键树(3-节点)中插入新键有三种子情况:
1.新键大于原树中的两个键,被连接到3-节点的右链接,只需要改变颜色即可。
2.新键是最小的,将上层的红链接右旋转,再改变颜色。
3.新键介于两者之间,将下层的红链接左旋转,再将上层的红链接右旋转,再改变颜色。
一棵大小为N的红黑树的最大高度不会超过2logn(最坏情况是树的最左边的路径节点全都是3-节点),根节点到任意节点的平均路径长度为1logn
3.散列表