文章目录
七个基于比较的排序算法
前期准备
使用公共接口方便测试,在Sort中定义cmp方法可以使排序算法代码更简洁。
//定义公共接口
public abstract class Sort<E extends Comparable<E>> {//强制要求传入的E类具有可比较性
protected E[] array;
private int cmpCount;
private int swapCount;
public void sort(E[] array) {
if(array==null||array.length<2) return;
this.array=array;
sort();
}
//
protected int cmp(int i1,int i2) {
cmpCount++;
return array[i1].compareTo(array[i2]);
}
protected int cmp(E v1,E v2) {
cmpCount++;
return v1.compareTo(v2);
}
protected void swap(int i1,int i2) {
swapCount++;
E temp=array[i1];
array[i1]=array[i2];
array[i2]=temp;
}
public E[] get() {
return array;
}
@Override
public String toString() {
StringBuffer sb =new StringBuffer();
sb.append("[");
for(int i=0;i<array.length;i++) {
if(i!=0) {
sb.append(" ");
}
sb.append(array[i]);
}
sb.append("]");
return sb.toString();
}
public int getCmpCount() {
return cmpCount;
}
public int getSwapCount() {
return swapCount;
}
protected abstract void sort();
}
1.冒泡排序
冒泡排序的思想:
第一轮从index=0开始遍历待排序数组,比较区间为0到array.length-1(即数组中所有元素)的元素, 每次对待排序数组中相邻的两个元素进行比较,如果当前元素比它的下一个元素(即后继 )大,就将两个元素交换位置,否则不进行任何操作。第一轮 “冒泡” 过程将使最大元素交换到数组末尾(即归位),然后开始第二轮,比较区间0到array.length-2(即除去已经归位的最大值,而对剩余的数据进行比较)。第二轮将使次大值元素归位。循环执行,直到只剩一个元素。
冒泡排序1
public class BubbleSort1<E extends Comparable<E>> extends Sort<E> {
protected void sort() {
//i从length-1到1,j每次从0到i,这样每轮循环第i+1个位置的元素就会被确定。
for(int i=array.length-1;i>0;i--) {
for(int j=0;j<i;j++) { //j<i,而不是j<=i
if(cmp(j, j+1)>0) {
swap(j, j+1);
}
}
}
}
}
冒泡排序2
如果在冒泡排序过程进行结束前,所有元素提前有序,提前退出。
public class BubbleSort2 <E extends Comparable<E>>extends Sort<E> {
protected void sort() {
boolean flag=true;
//i从length-1到1,j每次从0到i,这样每轮循环第i+1个位置的元素就会被确定。
for(int i=array.length-1;i>0;i--) {
flag=true;
for(int j=0;j<i;j++) { //j<i,而不是j<=i
if(cmp(j, j+1)>0) {
swap(j, j+1);
flag=false;
}
}
//如果在一轮循环中没有执行交换,说明数据已经全部有序,直接退出循环
if(flag) {
break;
}
}
}
}
冒泡排序3
分析可知,在每次执行冒泡排序过程时,可能尾部已有许多元素提前有序(并不是数组中所有元素),此时,在语句内部更改i值,以后循环将不再遍历到提前有序的部分尾部元素。
public class BubbleSort3<E extends Comparable<E>> extends Sort<E> {
protected void sort() {
//i从length-1到1,j每次从0到i,这样每轮循环第i+1个位置的元素就会被确定。
for(int i=array.length-1 ; i > 0 ; i--) {
int SortedIndex=0;//此处对SortedIndex赋初值是为了实现当数组完全有序的时候能直接退出循环。
for(int j=0; j < i ; j++) { //j<i,而不是j<=i
if(cmp(j, j+1)>0) {
swap(j, j+1);
SortedIndex=j+1;//每次交换后记录交换时的下标
}
}
i=SortedIndex;//更改i值
}
}
}
2.选择排序
选择排序的主要思想是每次从待排序的所有元素中选出最大值,将其与当前为排序部分的最后一个元素互换。
public class SelectionSort<E extends Comparable<E>> extends Sort<E> {
protected void sort() {
int maxIndex;
for(int i=array.length-1;i>0;i--) {
maxIndex=0;//需要记录下标,假设第一个元素为最大值
//遍历查找,找出目前未排序的元素中最大值
for(int j=1;j <= i;j++) {
if(cmp(j, maxIndex)>=0) {//使用 >= 可以使算法获得稳定性
maxIndex=j;
}
}
//如果最大值不是第i个元素,即将最大值放到下标为i的位置
//如果最大值是下标为i的元素,就不需要执行if语句内的代码
if(maxIndex!=i) {
swap(maxIndex, i);
}
}
}
}
3.堆排序
堆排序可以说是对选择排序的优化,它使用堆来获取待比较元素中的最大值
该算法策略是:
*1.原地建立堆
*2.将堆顶元素(下标为0)和下标为heapSize-1位置的元素互换(互换后当前最大值就被放到了当前未排序部分的末尾,即归位)。
*3.heapSize–
*4.堆下标为0的位置使用下滤操作恢复堆的性质
public class HeapSort<E extends Comparable<E>> extends Sort<E>{
private int heapSize;//记录堆中元素个数
@Override
protected void sort() {
//原地建堆
heapSize=array.length;
for(int i=(heapSize>>1)-1;i>=0;i--) {
ShifDown(i);
}
while(heapSize>1) {
//交换位置
swap(0, heapSize-1);
//堆中元素减少1
heapSize--;
//堆顶元素下滤
ShifDown(0);
}
}
//将index元素下滤过程
private void ShifDown(int index) {
int half=heapSize>>1;//完全二叉树中,非叶子节点的数量为floor(n/2)
E node =array[index];
//index小于第一个叶子节点的下标
while(index < half) {//保证当index有子节点时,才能进入该循环
//取出其子节点中的较大值记为Chlid
int ChildIndex=(index<<1)+1;//默认取左子节点,
E Child =array[ChildIndex];
if((ChildIndex+1) < heapSize) {//如果有右子节点
int right=ChildIndex+1;
if(cmp(array[right], Child)>0) {
Child=array[right];
ChildIndex=right;
}
}
//此时找到个子节点中的较大值
if(cmp(Child, node)<=0) {//当较大子节点小于或等于node时,
break;
}
//将较大孩子上移,同时index更新
array[index]=Child;
index=ChildIndex;
}
//最后退出循环,将node,放到合适位置
array[index]=node;
}
}
4.插入排序
插入排序可以类比玩扑克牌时齐牌过程,每次翻开一张牌,就将它插入到手里有序牌的合适位置,使手里牌数加1,仍然保持有序。
插入排序1
插入排序的思想是从第二个元素开始扫描,每次将该元素与其之前的元素依次比较,如果该元素比其之前元素小,就交换它们,再继续往前比较。
public class InsertSort1<E extends Comparable<E>> extends Sort<E> {
@Override
protected void sort() {
for(int i=1;i<array.length;i++) {
int current=i;
//当current大于0且current下标对应的值小于它的前一个值时,进行交换
while(current>0&&cmp(array[current], array[current-1])<0) {
swap(current, current-1);
current--;
}
}
}
}
插入排序2
分析可知,对于插入排序,不必每次交换,而是将大于当前元素的元素依次向后挪动,直到找到当前元素的合适插入位置,再将当前元素插入即可。这样避免了许多不必要的交换。
public class InsertSort2<E extends Comparable<E>> extends Sort<E> {
@Override
protected void sort() {
for(int begin=1;begin<array.length;begin++) {
int cur=begin;
E numble=array[cur];
//当cur大于0且cur下标对应的值小于numble时,进行挪动
while(cur>0&&cmp(numble, array[cur-1])<0) {
array[cur]=array[cur-1];
cur--;
}
array[cur]=numble;
}
}
}
插入排序3
继续分析可知,对于当前元素的插入位置的查找,可以通过二分法查找,用二分法找到对应的下标位置后,再将其后面的有序部分后移,然后将当前元素插入。
public class InsertSort3<E extends Comparable<E>> extends Sort<E> {
@Override
protected void sort() {
for(int begin=1;begin<array.length;begin++) {
E value=array[begin];//保存当前待插入元素
//使用二分查找找到合适的位置
int index=search(begin);
//元素挪动
for(int i=begin;i>index;i--) {
array[i]=array[i-1];
}
array[index]=value;
}
}
//该查找算法通过传下标来查找合适的从插入位置,因为传过来的下标位置就是end的值
//该算法将返回第一个大于array[index] 值的下标
private int search(int index) {
int begin=0;
int end=index;
while(begin<end) {
int middle=(begin+end)>>1;
if(cmp(array[index], array[middle])<0) {
end=middle;
}else {//如果大于或等于
begin=middle+1;
}
}
return begin;
}
}
5.快速排序
快速排序通过以一个元素(记为 v )为基准将大于它的元素放到它的右边,小于它的元素放到它的左边,使得该元素正确的在最后的结果有序数组中归位。然后再分别对该元素左边和右边的子数组进行同样的操作:即递归进行取一个元素(记为 v ) ,将大于它的放右边,小于它的放左边这样的操作。
public class QuickSort<E extends Comparable<E>> extends Sort<E> {
@Override
protected void sort() {
sort(0,array.length);
}
private void sort(int begin,int end) {
if(end-begin<2) return;
//调用pivotIndex方法对begin到end 区间内的数据进行处理,将begin下标元素放到合适位置,
//再以该下标位置为分界线递归执行sort方法
int middle=pivotIndex(begin, end);
//之前这两个sort错误地写成了quicksuort,是因为对递归的理解不够
sort(begin, middle);
sort(middle+1, end);
}
private int pivotIndex(int begin,int end) {
//获取一个在 [begin,end)之间的一个随机数,让begin下标与begin+random处的元素交换,达到每次取的pivot元素随机
//这可以避免每次划分产生极度不均匀的情况,使得最坏复制度的情况O(n^2) 不容易出现
int random=(int) Math.random()*(end-begin);
swap(begin, begin+random);
E pivot =array[begin];
int left=begin;
int right=end-1;
while(left<right) {
while(left<right&&cmp(right, begin)>0) right--;
if(left<right) array[left++]=array[right];//右边遇到小于或等于就进行交换
while(left<right&&cmp(left, begin)<=0) left++;
if(left<right) array[right--]=array[left];//左边遇到大于等于就交换
}
array[left]=pivot;
return left;
}
}
6.归并排序
归并排序采用先划分后合并的思想,先将整个待排序数组划分为许多个自然有序的数组(每个数组只有一个元素),再通过两两合并为若干个含两个元素的有序数组,继续合并直到数组变为1个,其结果即为待排序数组对应的有序数组。对于两两合并的过程,先将左边(两两合并过程中两个数组总是相邻的)的数组复制一份,然后使用两个指针分别指向两个数组的第一个元素,同时使用一个指向当前待排序空间第一个位置的指针(实例中重复利用begin作为该指针),每次比较将较小元素放到结果数组中。如果最后左边元素有剩余,将剩余元素依次复制到结果数组中;如果最后右边元素有剩余,什么都不用做,因为它们就在结果数组中,而且在正确的位置上。
public class MergeSort<E extends Comparable<E>> extends Sort<E> {
private E[] assits;//用于存放两两合并时左边部分的数组
@SuppressWarnings("unchecked")
@Override
protected void sort() {
//最大左边部分的数组长度为array.length/2;
assits=(E[]) new Comparable[array.length>>1];
sort(0,array.length);
}
private void sort(int begin,int end){
if(end-begin<2) return;
int middle=(end+begin)>>1;
sort(begin,middle);//递归划分左边
sort(middle,end);//递归划分右边
merge(begin, middle, end);//合并左边和右边
}
//begin指向第一块的第一个元素,middle指向第一块数组的最后一个元素的下一个位置
//middle指向第二块的第一个元素,end指向第二块的最后一个元素的下一个位置
private void merge(int begin, int middle,int end) {
//用一个辅助数组,存放 [begin,middle) 的元素
int k=0;
for(int i=begin;i<middle;i++) {
assits[k++]=array[i];
}
//到此,k表示assists中的元素个数
int left=0;//指向左边部分的第一个元素
int right=middle;//指向右边部分的第一个元素
while(left<k && right<end) {
if(cmp(assits[left], array[right]) <= 0) {// <= 使算法具有稳定性
array[begin++]=assits[left++];
}else {//cmp(assits[left], array[right])>0
array[begin++]=array[right++];
}
}
if(left<k) {
array[begin++]=assits[left++];
}
//因为采用了合适的策略,即将待排序的两块中左边块取出,右边块未取出
//所以当right<end时什么也不用做
}
}
7.希尔排序
希尔排序的重要理论基础是:分析插入排序算法可知,在对数组进行插入排序时,待排序数组中逆序对越少,插入排序需要插入的次数就越少。
希尔排序利用步长将待排序数组分成一个个子数组(每个数组中的元素之间的距离不再是+1,而是加步长),分别对每个子数组采用插入排序,一次在某个步长下排序结束后,每个子数组都变得有序,然后减小步长继续进行同样的排序,直到步长为1。使用步长的作用是在每次步长减小之前(步长减小对应插入数据规模扩大,数据每次插入移动次数变多)的排序中使逆序对数量减少。
在这个排序中,步长数组的选取会影响排序算法的时间复杂度,目前已知的最好的步长数组的选取为按照以下公式计算的结果:
public class ShellSort<E extends Comparable<E>> extends Sort<E> {
@Override
protected void sort() {
//调用方法获取步长数组
ArrayList<Integer> stepSequence=SedgeStepSequence();
//遍历stepSequence数组,每次取出一个步长值,进入sort中利用该步长进行排序
for(Integer step:stepSequence) {
sort(step);//依次取出步长数组中的元素,将步长元素传入sort(int)方法中进行排序
}
}
//利用步长对数组进行排序的方法
private void sort(int step) {
//对每个组中的元素排序,插入排序
//col表示每个子数组第一个元素
for(int col=0;col<step;col++) {
//begin从每个组的下一个元素开始(每个组的下标差为step),
for(int begin=col+step;begin<array.length;begin+=step) {
int cur=begin;
//此处插入排序采用最简单的冒泡排序直接交换相连两个元素,没有经过优化,为了便于理解
while(cur>col&&cmp(cur, cur-step)<0) {//当当前元素下标大于col时且当前元素值小于前一个元素时,进行交换
swap(cur, cur-step);
cur-=step;
}
}
}
}
//希尔提供的步长
@SuppressWarnings("unused")
private ArrayList<Integer> ShellStepSequence() {
ArrayList<Integer> stepSequence=new ArrayList<>();
//构造步长数组,采用除以2的策略,
int step=array.length;
while((step >>= 1)>0) {//每次除以2
stepSequence.add(step);
}
return stepSequence;
}
//目前最优步长,
private ArrayList<Integer> SedgeStepSequence() {
ArrayList<Integer> stepSequence=new ArrayList<>();
int count=array.length;
int k=0;
int step=0;
while(true) {
if(k%2==0) {//如果k为偶数 ,采用公式 : 9*(2^k-2^(k/2))+1 进行计算
int pow=(int)Math.pow(2, k>>1);
step=1+9*(pow*pow-pow);
}else {//如果k为奇数 ,采用公式 : 8*2^k-6* 2^((k+1)/2) +1;
int pow1=(int) Math.pow(2, k);
int pow2=(int) Math.pow(2, (k+1)>>1);
step=8*pow1-6*pow2+1;
}
if(step>=count) break;
stepSequence.add(0,step);
k++;
}
return stepSequence;
}
}
三个非比较的排序算法
1 计数排序
计数排序通过将密集数据计数的方式完成排序。即创建一个长度为 待排序数组中元素最大值 的数组(记为counts)用来计数,通过遍历待排序数组,以待排序数组中的元素值 作为counts的元素下标,使counts中对应下标位置的值加1,最后通过遍历counts数组,将其中元素值为非0的下标作为结果数组的值,依次取出放入到结果数组中。
计数排序1
计数排序最简单的实现如下,该实现有几个缺陷:
*1.无法对负整数进行排序
*2.极其浪费空间
*3.是不稳定的排序
public class CountingSort extends Sort<Integer> {//计数排序
@Override
protected void sort() {
//找出最大值
int max=array[0];
for(int i=0;i<array.length;i++) {
if(array[i]>max) {
max=array[i];
}
}
//以最大值作为数组的最大下标创建数值
int[] counts=new int[max+1];
//记录元素出现个数
for(int i=0;i<array.length;i++) {
counts[array[i]]++;//让下标为array[i]所指向的元素+1
}
//遍历counts数组,将其中元素依次取出放入到结果数组中
int index=0;
for(int i=0;i<counts.length;i++) {
while(counts[i]-->0) {
array[index++]=i;
}
}
}
}
计数排序2
分析可知,在计数排序1中,如果找到元素待排序数组中元素最小值,每次通过array[i]-min 来计算 array[i] 对应的在counts中的下标,会一定程度避免空间浪费,同时也可以完成对负数的存储。对counts(计数数组)深入分析可知,counts中每个下标对应位置存储的值就是counts下标值在arrray中出现的次数,而若从左到右让counts中所有元素执行 counts[i]+=counts[i-1]; 的操作,其结果是每个counts中下标对应位置存储的值不再是counts下标值在arrray中出现的次数,而是counts下标值在结果数组中的位置。
为了使该排序算法获得稳定性,所采取的策略是:在counts完成储存位置信息的前提下,从后往前遍历待排序数组,每遍历到一个元素就让与对应的在counts中下标位置的值减1作为结果位置,放到结果数组中的对应位置。通过这样的策略,在对待两个相等数据时,可以保留其在待排序数组中的相对信息(因为从后往前遍历,在待排序数组中前面的元素,计算得到的相对位置要小)。
counts[i]+=counts[i-1];之所以能使结果counts下标对应位置存储的值能存储counts下标值在结果数组中的位置,是因为在存储下标值在arrray中出现的次数的情况下,如果对于counts的某个下标位置,让该位置的值加上前面所有位置的值,就得到了它相对于前面所有元素的相对位置, 该相对位置值即表示它在结果数组中是第几个元素,用这个值减1就得到其在结果数组中的正确下标值。
public class CountingSort2 extends Sort<Integer> {
@Override
protected void sort() {
//找出待排序数组中的最小元素和最大元素
int max=array[0];
int min=array[0];
for(int i=0;i<array.length;i++) {
if(array[i]>max) {
max=array[i];
}
if(array[i]<min) {
min=array[i];
}
}
//以最大元素和最小元素的值作为求数组长度的参数创建一个记数数组
int[] counts=new int[max-min+1];
//记录个数,array中每个元素在counts中对应的值是array[i]-min
for(int i=0;i<array.length;i++) {
counts[array[i]-min]++;//让下标为array[i]-min所指向的元素+1
}
//到此,counts数组的计数过程结束
//下面将counts数组中的元素进行一个操作,让每个位置的值等于前面计数值之和,使得counts中元素不再储存每个下标对应的出现次数,而是每个下标在结果数组中的相对位置
for(int i=1;i<counts.length;i++) {
counts[i]+=counts[i-1];
}
//在对array进行排序之前,还需要创建与array同样大小的数组用于对排序结果进行存储
int[] newArray=new int[array.length];
//现在从后往前遍历arrray,每取到一个元素,就用其元素值与min进行运算,获得在counts中的下标
//再用此下标进行 -1 操作获得该元素在结果数组中的最终位置,这里一定要用自减方式,因为-1的结果要覆盖原来的数据,是为了当出现重复数据时后一个数据需要使用前一个数据计算的结果。
for(int i=array.length -1; i>=0;i--) {
newArray[--counts[array[i]-min]]=array[i];
}
//将newArray中的元素复制到array中
for(int i=0;i<array.length;i++) {
array[i] = newArray[i];
}
}
}
2 桶排序
桶排序是基于桶的(桶可以是数组也可以用链表),基本思想是先按照一定策略(不同实现策略不同)将数据分放到各个桶中,然后对桶中元素进行排序,使得每个桶分别有序,然后遍历依次遍历每个桶,将各个桶中元素依次取出放入结果数组中,就获得了待排序数组对应的有序数组。
以下实例实现对 [0 , 1) 之间的小数的排序,分桶方式是用元素值乘以数据长度作为该元素对应桶的下标,对各个桶的排序使用比较简单的选择排序。使用了一个counts数组来存储每个桶中的元素个数信息。
/*
* 该桶排序实现了对 [0 , 1) 之间的数据进行排序(不同的数据有不同的实现方式)
*/
public class BucketSort extends Sort<Double> {
@SuppressWarnings("unchecked")
@Override
protected void sort() {
//创建一个桶数组,同时为每个桶开辟空间,桶数组中每个元素都是一个数组
ArrayList<Double>[] buckets =new ArrayList[array.length];
//为每个桶开辟空间
for(int i=0;i<buckets.length;i++) {
buckets[i] = new ArrayList<Double>(array.length);
}
//创建一个用来记录每个桶中元素个数的数组
int[] counts =new int[array.length];
//将待排序数组中所有元素依次放入到桶中
for(int i=0;i<array.length;i++) {
int bucketIndex = (int)(array[i] * array.length);
buckets[bucketIndex].add(array[i]);//将array[i] 放入下标为bucketIndex的桶中
counts[bucketIndex]++;
}
//分别对每个桶进行排序
for(int i=0;i<counts.length;i++) {//遍历所有的桶
if(counts[i]>1) {//如果对应的桶中有大于两个元素,就对他们进行排序
for(int end=1;end<counts[i];end++) {//循环遍历桶中所有元素,对桶中所有元素进行选择排序
//使用的是插入排序1(简单的一个一个交换的方式)
int cur= end;
double numble= buckets[i].get(cur);
while(cur>0&& numble < buckets[i].get(cur-1) ) {
double temp=buckets[i].get(cur-1);
buckets[i].add(cur, temp);
cur--;
}
buckets[i].add(cur,numble);
}
}
}
//将桶中的元素依次放入到结果数组中,按照从左到右遍历桶,每个桶用下标由小到达方式
int index=0;
for(int i=0;i<counts.length;i++) {
if(counts[i]>0) {
for(int j=0;j<counts[i];j++) {
array[index++]=buckets[i].get(j);
}
}
}
}
}
3 基数排序
- 基数排序的基本思想是先按照个位数的大小对数组中数据进行排序, 再按照十位数大小对数据进行排序,再按照百位,千位…
计算每一位的方式是:值 / 权 % 10
下面的算法采用计数排序辅助进行基数排序
public class radixSort extends Sort<Integer> {
@Override
protected void sort() {
//获取数组中的最大元素
int max=array[0];
for(int i=0;i<array.length;i++) {
if(array[i]>max) {
max=array[i];
}
}
//每次传入一个divisor,当divisor大于数组中的最大值时结束循环
for(int divisor=1;divisor<=max;divisor *= 10) {
CountingSort(divisor);
}
}
//对待排序数组中的所有数据按照某一位使用优化后的计数排序对其排序
protected void CountingSort(int divisor) {
//开辟10个空间,基数从0到9,有10个元素
int[] counts=new int[10];
//记录个数
for(int i=0;i<array.length;i++) {
//使用array[i]/divisor%10计算出对应位置的基数值,以其值作为counts中的元素下标,让该下标对应位置的元素值+1
counts[array[i]/divisor%10]++;
}
//到此,counts数组的计数过程结束
//下面让每个位置的值等于前面计数值之和,这样方便获得下标
for(int i=1;i<counts.length;i++) {
counts[i]+=counts[i-1];
}
//在对array进行排序之前,还需要创建与array同样大小的数组用于对排序结果进行存储
int[] newArray=new int[array.length];
//现在从后往前遍历arrray,每取到一个元素, 获得在counts中的下标
//再用此下标进行 -1 操作获得该元素在结果数组中的最终位置
/*
* 此处需要注意的是虽然counts数组是使用待排序数组中元素的某一位作为下标进行计数的,
* 但是在赋值的排序过程赋值的时候,是将待排序数组中的对应元素复制到结果数组中,而不是只复制其元素的某一位
*/
for(int i=array.length -1; i>=0;i--) {
//array[i]/divisor%10为该元素对应的在counts中的位置,counts中的该值减1得其在结果数组中的最终位置
//只能用自减操作,因为需要覆盖该位置的原值,一便下次取到相同值时使用
newArray[--counts[array[i]/divisor%10]]=array[i];
}
//将结果数组放会array中
for(int i=0;i<array.length;i++) {
array[i] = newArray[i];
}
}
}