本篇共介绍一下几种排序:
插入排序:直接插入排序、希尔排序。
选择排序:直接选择排序、堆排序。
交换排序:冒泡排序、快速排序。
归并排序。
基数排序。
直接插入排序:
思路:对于一组数据,先将第一个和第二个数排序,再将第三个数插入之前已经有序序列中形成新的有序序列,以此类推。也就是对于第n个数的排序,是将这第n个数插入到前n-1个数的有序序列中,从而得到一个n个数的有序序列。
代码实现:
public int[] insert_sort(int num[])
{
int l=num.length;
for(int i=1;i<l;i++)
{
int insertnum=num[i];//需要插入的元素
int j=i-1;//已经排好序的序列元素个数
while(j>=0&&num[j]>insertnum)
{
num[j+1]=num[j];
j--;
}
num[j+1]=insertnum;
}
return num;
}
性能分析:
最好的情况:原来数组已经有序,复杂度为O(n);
最坏的情况:原数组为逆序,复杂度为;
平均时间复杂度为,空间复杂度为O(1)。
稳定性:稳定
希尔排序
思路:希尔排序是针对直接插入排序中逆序情况下效率较低而改进而来的。假设数据的个数为n,选取加一个奇数k=n/2,将下标之差为k的数分为一组进行简单插入排序并构成有序序列,如比较下标为1和1+k的两个数据。之后再去k=k/2,将下标差值为k的数分为一组进行简单插入排序构成有序序列,重复进行直到K=1时,此时序列已经基本有序,再执行简单插入排序时效率较高。但是在不同的排序中数会移动,因此是不稳定的。
代码如下:
public static int[] shell_sort(int num[])
{
int l=num.length;
while(l!=0)
{
l=l/2;
for(int i=0;i<l;i++)//进行分组
{
for(int j=i+l;j<num.length;j+=l)
{
int k=j-l;//已排序好的有序序列的最后一个
int temp=num[j];//等待插入的元素
while(k>=0&&temp<num[k])
{
num[k+l]=num[k];
k-=l;//移动l位
}
num[k+l]=temp;
// for(int q=0;q<num.length;q++)
// System.out.print(num[q]+" ");
// System.out.println();
}
}
}
return num;
}
性能分析
最好情况:O(nlog n);
最坏情况:O(nlog n);
平均时间复杂度:O(nlog n);
空间复杂度:O(1)
稳定性:不稳定;
直接选择排序
思路:第一次遍历整个数组找到最小的一个元素,将其与第一个元素交换位置;第二次从第二个位置开始寻找第二小的元素,将其与第二个元素交换位置,以此类推,直到找到最后一个元素为止。
代码如下:
public int[] choose_shrt(int num[])
{
int len=num.length;
for(int i=0;i<len;i++)
{
int p=i;
int min=num[i];
for(int j=i+1;j<len;j++)
{
if(num[j]<min)
{
p=j;
min=num[j];
}
}
num[p]=num[i];
num[i]=min;
}
return num;
}
性能分析:
最好情况:O(n^2)
最坏情况:O(n^2)
平均时间复杂度:O(n^2) 空间复杂度:O(1)
稳定性:不稳定
堆排序
思路:堆可以分为大顶堆和小顶堆,大顶堆中最大的元素在堆顶,小顶堆中最小的元素在堆顶。首先按照数组的序号从上到下,从左到右建立一个初始堆,类似于完全二叉树的结构,但节点之间没有指针关系,只是把数组摆成了堆的形式。之后从下到上调整堆,分别拿双亲节点和子节点比较大小,如果双亲节点小于最大子节点,则交换位置。这样到最后,我们会得到一个初始的大根堆。
接下来我们把堆顶元素和数组的最后一个元素交换位置,除此最后一个数外,我们继续调整堆,使其满足大根堆的条件,再把堆顶元素和数组倒数第二个元素交换,以此类推,最终整个数组从小到大排序。
由于存在大量的数据交换,因此是不稳定的。
参考代码:
public void heapSort(int[] a){
int len=a.length;
//循环建堆
for(int i=0;i<len-1;i++){
//建堆
buildMaxHeap(a,len-1-i);
//交换堆顶和最后一个元素
swap(a,0,len-1-i);
}
}
//交换方法
private void swap(int[] data, int i, int j) {
int tmp=data[i];
data[i]=data[j];
data[j]=tmp;
}
//对data数组从0到lastIndex建大顶堆
private void buildMaxHeap(int[] data, int lastIndex) {
//从lastIndex处节点(最后一个节点)的父节点开始
for(int i=(lastIndex-1)/2;i>=0;i--){
//k保存正在判断的节点
int k=i;
//如果当前k节点的子节点存在
while(k*2+1<=lastIndex){
//k节点的左子节点的索引
int biggerIndex=2*k+1;
//如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在
if(biggerIndex<lastIndex){
//若果右子节点的值较大
if(data[biggerIndex]<data[biggerIndex+1]){
//biggerIndex总是记录较大子节点的索引
biggerIndex++;
}
}
//如果k节点的值小于其较大的子节点的值
if(data[k]<data[biggerIndex]){
//交换他们
swap(data,k,biggerIndex);
//将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值
k=biggerIndex;
}else{
break;
}
}
}
}
性能分析:
最好情况:O(nlog n)
最坏情况:O(nlog n)
平均情况:O(nlog n)
空间复杂度:O(1)
稳定性:不稳定
冒泡排序
思路:从数组的第一个数开始依次对所有的数进行两两比较,将大的数放后面。如先将第一个元素和第二个元素进行比较,将较大的元素放在第二位,之后再将之前比较后的第二个元素与第三个元素比较,较大的元素放在第三的位置,以此类推。第一轮排序的结果是最大的元素出现在数组的最后,第二轮的结果是第二大的元素出现在倒数第二的位置.....因为这样的流程有点像“冒泡”,因此成称为冒泡排序。
由于相同的元素不会调换位置,因此是稳定的 ,但是效率较低。
代码如下:
public int[] bubble_sort(int num[])
{
int len=num.length;
int temp;
for(int i=0;i<len;i++)
{
for(int j=0;j<len-i-1;j++)
{
if(num[j]<num[j+1])
{
temp=num[j];
num[j]=num[j+1];
num[j+1]=temp;
}
}
}
return num;
}
性能分析:
最好情况:O(n^2)
最坏情况:O(n^2)
平均情况:O(n^2)
空间复杂度:O(1)
稳定性:稳定
快速排序
思路:快速排序是基于递归和分治来实现的。首先选取一个基准值保存在某一个变量中,假设选取数组中的第一个数作为基准值。之后使用两个指针,右指针从最后一个元素开始向左移动(如果选取第一个元素作为基准值,那么一定要右指针先移动。因为如果此时第一个元素为最小值时,先移动左指针寻找大的元素时会将指针移动到第二个元素后停下,此时右指针寻找较小元素时会一直移动直到移动到第二个元素时与左指针相遇,这时再调换两指针的值时便会发生错误),寻找比基准值小的第一个元素停下,此时左指针从左边第一个数开始向右移动,寻找比基准值大的第一个元素后,将两指针所指向的值调换。一直移动直到两指针相遇,此时将基准值和相遇位置的值调换位置,此时第一轮快速排序就结束了,第一轮的结果是将数组分成了两部分:一部分比基准值大,一部分比基准值小。也就是将基准值的位置确定了。之后再对两部分的数组再进行下一轮的快速排序,直到有序。
快速排序在数组乱序的情况下有很高的效率,但它在排序中涉及到了数的交换,可能会调换相同元素的相对位置,因此也是不稳定的。
代码如下:
public void quick_Sort(int[]a,int start,int end){
if(start<end){
int base=a[start];//选基准值
int temp;//记录中间值
int i=start;
int j=end;
do{
while((a[j]>=base)&&j>start){
j--;
}
while((a[i]<=base)&&i<end){
i++;
}
if(i<=j){
temp=a[i];
a[i]=a[j];
a[j]=temp;
i++;
j--;
}
}while(i<=j);
if(start<j){
quick_Sort(a,start,j);
}
if(end>i){
quick_Sort(a,i,end);
}
}
}
性能分析:
最好情况:O(nlog n)
最坏情况:O(n^2)
平均情况:O(nlog n)
空间复杂度:O(1)
稳定性:不稳定
归并排序
思路:归并排序也是基于递归和分治的思想,但是和快速排序有些许的不同。快速排序是利用基准将数组划分为两部分,再在两部分中分别递归;而归并排序是先不断利用二分将数组化为多个最小部分,先在最小部分中实现有序,之后再进行回溯,将两个最小单位合并成一个有序的长度为2倍最小单位长度的数组,依次类递归,直到递归到最初的位置便归并为一个有序的数组。大体思想是先将数组化为为多个小数组,再通过归并小数组自下而上地排序。
实现过程:
1.先申请一个大小为两个已排序的数组长度之和的数组,用于存放归并后的序列
2.设置两个指针,分别指向两个已排序数组的第一个元素
3.比较两个指针的元素的大小,较小的一个值取出放入合并后的空间,并且将指针向后移动一位
4.重复第三步,直到某一个指针都到数组尾部
5.将另一个数组的所有剩下的元素复制到合并后的数组中
参考代码:
//归并排序
public static void mergeSort(int[] data) {
sort(data, 0, data.length - 1);
}
public static void sort(int[] data, int left, int right) {
if (left >= right)
return;
// 找出中间索引
int center = (left + right) / 2;
// 对左边数组进行递归
sort(data, left, center);
// 对右边数组进行递归
sort(data, center + 1, right);
// 合并
merge(data, left, center, right);
// for (int i = 0; i < data.length; i++) {
// System.out.print(data[i] + "\t");
// }
// System.out.println();
}
/**
* 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
*/
//data为数组对象,left为左数组的第一个元素的索引,center为左数组最后一个元素的索引,
//center+1为右数组的第一个元素的索引,right为右数组最后一个元素的索引
public static void merge(int[] data, int left, int center, int right) {
// 临时数组
int temp[] = new int[data.length];
// 右数组第一个元素索引
int mid = center + 1;
// third 记录临时数组的索引
int third = left;
// 缓存左数组第一个元素的索引
int tmp = left;
while (left <= center && mid <= right) {
// 从两个数组中取出最小的放入临时数组
if (data[left] <= data[mid]) {
temp[third++] = data[left++];
} else {
temp[third++] = data[mid++];
}
}
// 将另一数组的剩余部分依次放入临时数组,其实只会执行其中的一个
while (mid <= right) {
temp[third++] = data[mid++];
}
while (left <= center) {
temp[third++] = data[left++];
}
// 将临时数组中的内容拷贝回原数组中
// (原left-right范围的内容被复制回原数组)
while (tmp <= right) {
data[tmp] = temp[tmp++];
}
}
性能分析:
最佳情况:O(nlog n)
最坏情况:O(nlog n)
平均情况:O(nlog n)
空间复杂度:O(n)
稳定性:不稳定
基数排序
思路:基数排序不是一种传统意义上的排序,而更像是一种排序的应用,基数排序的总体思路就是将待排序数据拆分成多个关键字进行排序,也就是说,基数排序的实质是多关键字排序。
多关键字排序将待排序的数据拆分成多个关键字后进行排序,根据子关键字对待排序数据进行排序。
多关键字排序有最高位优先和最低位优先两种:
比如,我们对于 123,23,3进行排序,我们看到这些数据最高直到百位,因此我们可以选择百位、十位、个位等三个关键字进行比较,先比较百位的大小,如果相同再比较十位,如果还相同再比较个位。这种为最高位优先,但是当我们开始比较十位的时候,程序还需要回头去判断百位是否会相同,这样就比较麻烦,因此一般都采用最低位优先。
基数排序方法对任一子关键字排序时必须借助于另一种排序方法,而且这种排序方法必须是稳定的。但是如果有负数存在的话,会对算法有较大的影响。
代码实现:
private static void radixSort(int[] array,int d)
{
int n=1;//代表位数对应的数:1,10,100...
int k=0;//保存每一位排序后的结果用于下一位的排序输入
int length=array.length;
int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
int[] order=new int[length];//用于保存每个桶里有多少个数字
while(n<d)
{
for(int num:array) //将数组array里的每个数字放在相应的桶里
{
int digit=(num/n)%10;
bucket[digit][order[digit]]=num;
order[digit]++;
}
for(int i=0;i<length;i++)//将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
{
if(order[i]!=0)//这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
{
for(int j=0;j<order[i];j++)
{
array[k]=bucket[i][j];
k++;
}
}
order[i]=0;//将桶里计数器置0,用于下一次位排序
}
n*=10;
k=0;//将k置0,用于下一轮保存位排序结果
}
}
算法分析:
最好的情况:O(d(n+r))
最坏的情况:O(d(n+r))
平均时间复杂度:O(d(n+r))
空间复杂度:O(n+r)
稳定性:稳定
总而言之,不同的算法有其不同的优点,具体采用哪种算法需要具体问题具体分析。