最近看数据结构的一些知识,有些简单的算法几乎忘差不多了。可能很少去研究,去思考,有的东西就会忘得很快,过一段时间归纳下就会清晰很多。于是还是以笔记的形式。
排序算法说多不多,常用的基本就那么几种,简单复杂的都有。有的简单,但时间复杂度高,有的可能复杂些,但是效率高。以下说说这些算法(通篇例子以升序为例):
目录
01.冒泡排序
所谓冒泡排序就跟冒泡是个道理。想象一下水底的泡泡慢慢冒出水面的过程,跟这个算法其实就是一个思想,不断将底部的数据调整到目的顺序类似冒泡。图中是该序列第一趟冒泡的整个过程,当i=1时将1冒到了顶端。
显然以后的每一趟都是重复这样的规则,每一趟都找出了待排序序列中最小的数,并且能使得大的数往下沉,小的数往上冒。那么很容易想到,这样执行只需要执行到没有数再交换位置或者最后一个数,序列便排序完毕。
public static void swap(int[] data,int i,int j)
{
if(i==j)
return;
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
// 冒泡排序,每次把剩下的无序中的最大那个数上浮到当前序列的最后的位置,直到没有数再交换就中止
public static void bubbleSort(int[] data)
{
int dlen = data.length;
boolean exchange = true;
for (int i=0;i<dlen&&exchange;i++)
{
exchange = false;
for(int j=0;j<dlen-i-1;j++)
{
if(data[j]>data[j+1])
{
swap(data,j,j+1);
exchange = true;
}
}
}
}
这样的算法,最好情况就是序列本来就有序,那么只需要比较一趟就可以完成序列排序。最坏情形倒序情形,每一趟都有数往上调整顺序,比较次数,时间复杂度 。
02.简单选择排序
举个例子,军训排序,从矮到高,教官这个时候开始找最矮同学放到排头,之后在剩下的队伍中找最矮的排到第二,依次类推。这便是简单选择排序的思想。
比较简单就不附上代码了。很容易分析得到比较次数,时间复杂度 。与冒泡排序不同的是,其最好最坏的情形都需要比较相应的次数。
// 简单选择排序每次在剩下的序列中选择最小的数放在剩下序列的排头
public static void SimpleSort(int[] data)
{
for(int i=0;i<data.length;i++)
{
int mdata = i;
for (int j=i+1;j<data.length;j++)
mdata = data[j]<data[mdata] ? j : mdata;
swap(data,mdata,i);//见冒泡排序中声明的该方法
}
}
03.直接插入排序
插入排序顾名思义,即插入式排序。还是前面的例子,故事是这样的:教官已经排完了,但是又来了一批新同学要加入队伍,这时要使得队伍有序,教官这时就用了插入排序的办法,拉过来一个同学便开始从尾到头开始比较身高,遇到又身高比手里这个同学还要矮的便放在那个同学后面。当然极端情况就是没有更矮便放在排头了。这样便把新来的同学插入到了原来有序的队列中形成了新队列。
如图所示1~7是升序排列,现在要把5,8,6,9插入到队列中。这时就是一个插入排序的过程,显然当5和7比,小于成立于是往前移,遇到4,小于不成立(遇到了更矮的)于是就将该数放到4后面,就使得插入数并保持旧序列有序。
//直接插入排序,每次从剩下序列中选择一个数插入已排好的序列中
// 选择第i+1插入前i个数中(前i个数有序)
// 从i~0依次比较,如果比a[i+1]大,则后退一位,如果相等或小于则退出比较
// 如果当前存在后退的时候,则在中止比较位置的后面插入a[i+1],否则该位置的数不变
public static void insertSort(int[] data)
{
int j,i;
for (i=0;i<data.length-1;i++)
{
int idata = data[i+1];
for(j=i;j>=0&&data[j]>idata;j--)
{
data[j+1] = data[j];
}
data[j+1] = j==i ? data[j+1]:idata;
}
}
显然最好的情况,队列已经有序,那么只需要每个数依次和前面的数比较一次,也就是n-1次,时间复杂度O(n), 但是最坏情形是逆序序列,这时需要比较, 时间复杂度。
04.希尔排序
希尔排序可能在现实中用得比较少,其核心精神就是增量排序。实质就是给定待排序的序列,选择一个增量从头开始和每一增量位置的数进行直接插入排序。每一趟过后缩小一半,直到最后增量为1执行最后一趟。
/***
* 希尔排序
* @param data
*/
private static void shellSort(int[] data)
{
int dk = data.length/2;
while(dk>=1){
shellInsertSort(data,dk);
dk = dk/2;
}
}
private static void shellInsertSort(int[] data, int dk)
{
for(int i=dk;i<data.length;i++)
{
if(data[i]<data[i-dk])
{
int temp = data[i];
int j = i-dk;
while (j>=0&&data[j]>temp)
{
data[j+dk] = data[j];
j = j-dk;
}
data[j+dk] = temp;
}
}
}
05. 堆排序
堆排序即将原序列以完全二叉树的模型(i的祖先结点为i/2,i=1,2,3……), 每次调整序列为最大根或最小根(O(logn)),即每一趟找出当前剩余数中的最大值或最小值,经过n次堆调整就完成了最终的排序。时间复杂度O(nlogn)。
//堆排序首先构建排序树,根节点为i,左右子结点为2i,2i+1
//排序方式一:可以通过依次插入结点到堆,然后从堆中取出数据排序
public static void heapSort(int[] data)
{
for (int i=2;i<=data.length;i++)
{
insertHeap(data,i,data[i-1]);
}
for(int j = data.length;j>0;j--)
{
data[j-1] = extractMax(data,j);
}
}
/***
* 将每个指定位置的点做上浮操作(跟祖先结点比较,判断是否需要移到靠近根的位置)
* @param data
* @param k 第k个结点,从1开始计数
*/
public static void shiftUp(int[] data,int k)
{
for (int i = k/2;i>0;i=i/2)
if(data[k-1]>data[i-1])
{
swap(data,i-1,k-1);
k = i;
}
}
/**
* 指定位置插入结点,然后调整结点位置,保持大根堆
* @param data
* @param index
* @param item
*/
public static void insertHeap(int[] data,int index,int item)
{
if(index>data.length)
return;
data[index-1] = item;
shiftUp(data,index);
}
public static int extractMax(int[] data,int ed)
{
int cdata = data[0];
data[0] = data[ed-1];
shiftDown(data,1,ed-1);
return cdata;
}
public static void shiftDown(int[] data,int k,int ed)
{
int j;
while (k<=ed)
{
if(k*2+1<=ed)
j = data[k*2]>=data[k*2-1]?k*2:k*2-1;
else if(k*2<=ed)
j = 2*k-1;
else
break;
if(data[j]>data[k-1])
{
swap(data,k-1,j);
k = j;
}else{
break;
}
}
}
06. 归并排序
对于一个序列,将其划分为子序列,每个子序列经过排序,两两合并成最终的有序序列。
/**
*
* @param data 目标序列
* @param grp1 组别1的起始序列号
* @param grp2 组别2的起始序列号
*/
public static void merge(int[] data,int grp1,int grp2){
int glen =grp2-grp1;
int ge1 = grp1+glen;
int ge2 = Math.min(grp2+glen,data.length);
int[] tempArray = new int[glen+ge2-grp2];
int i=grp1,j=grp2,t=0;
while (i<ge1&&j<ge2){
if(data[i]<=data[j]){
tempArray[t++]=data[i++];
}else{
tempArray[t++]=data[j++];
}
}
while(i<ge1){
tempArray[t++] = data[i++];
}
while(j<ge2){
tempArray[t++] = data[j++];
}
System.arraycopy(tempArray,0,data,grp1,glen+ge2-grp2);
}
/**
* 归并排序,每次两两合并子序列,最终使得序列有序
* @param data 目标序列
* @param groups 标记每个组在序列中的起始位置
*/
public static void mergeSort(int[] data,int[] groups){
if(groups.length==1)
return;
int[] grp = new int[groups.length/2+groups.length%2];
int i=1,j=0;
for(;i<groups.length;i=i+2){
merge(data,groups[i-1],groups[i]);
grp[j++]=groups[i-1];
}
if(i==groups.length){
grp[j]=groups[i-1];
}
mergeSort(data,grp);
}
07. 快速排序
快速排序相对应用较多 ,核心思想将序列中选出的数作为划分依据,将序列划分为比这个数大部分(右侧序列),和比这个数小的部分(左侧序列)。这样最终确定了这个数的位置,同时左右侧序列可以再次递归下去。每一趟时间复杂度O(n),需要递归O(logn)次,时间复杂度为O(nlogn)。
//快速排序,每次选择一个数作为基准,将序列分成左右两块,左边均小于该数,右边均大于该数
public static int partionInQuickSort(int[] data,int st,int ed)
{
swap(data,st, (int)(Math.random()%(ed-st+1))+st);//生成st~ed范围中的随机数
int j = st;
for (int i = st+1;i<=ed;i++)
{
if(data[i]<=data[st])
{
//依次查找比data[st]小的数,然后与比data[st]大的数交换;j标记最接近data[st]左边数的索引
j++;
swap(data,i,j);
}
}
swap(data,st,j);
return j;
}
public static void quickSort(int[] data,int st,int ed)
{
if(st>=ed)
return;
int index = partionInQuickSort(data,st,ed);
quickSort(data,st,index-1);
quickSort(data,index+1,ed);
}
最后是以上排序算法的测试主方法。
//测试主方法
public static void main(String[] args){
int[] data = new int[] {3,2,5,6,1,4,8,9,7,2};
// 冒泡排序
// int[] bdata = new int[data.length];
// System.arraycopy(data,0,bdata,0,data.length);
// bubbleSort(bdata);
// System.out.println(Arrays.toString(bdata));
// 简单选择排序
// int[] cdata = new int[data.length];
// System.arraycopy(data,0,cdata,0,data.length);
// SimpleSort(cdata);
// System.out.println(Arrays.toString(cdata));
// 直接插入排序
// insertSort(data);
// System.out.println(Arrays.toString(data));
// 快速排序
// quickSort(data,0,data.length-1);
// System.out.println(Arrays.toString(data));
// 堆排序
// heapSort(data);
// System.out.println(Arrays.toString(data));
// 希尔排序:每次给予一个步长,然后利用插入排序将子序列有序
// shellSort(data);
// System.out.println(Arrays.toString(data));
// 归并排序:将序列分成若干组,然后依次让每组都有序,最后依次合并
// 初始时,单个的数为一组,组标依次为该数索引号
int[] groups = new int[data.length];
for(int i=0;i<data.length;i++)
groups[i]=i;
mergeSort(data,groups);
System.out.println(Arrays.toString(data));
// 桶排序:初始化若干桶,将序列扔进这些桶中,让每个桶中的数都有序
// 计数排序:对于存在大量重复的数适用,每次按照数值大小顺序统计每个数的个数,最后依次返回原序列
// 基数排序:按照数位排序,先个位数,再按照十位数……
// System.out.println("123");
}
排序算法应用还是要根据实际情形。更多的排序算法,比如基数排序,桶排序,计数排序等实际应用中条件更具体,应用不是十分广泛,至少笔者工作业余项目中并没有用得十分多,这里不做详尽解说。读者有兴趣可以去查相关资料了解。这里可以参考这篇文章中的解说,文章附有动态图,理解会更轻松。