面试中常用排序算法实现(Java)

 当我们进行数据处理的时候,往往需要对数据进行查找操作,一个有序的数据集往往能够在高效的查找算法下快速得到结果。所以排序的效率就会显的十分重要,本篇我们将着重的介绍几个常见的排序算法,涉及如下内容:

  • 排序相关的概念
  • 插入类排序
  • 交换类排序
  • 选择类排序
  • 归并排序算法实现

一、排序相关的基本概念
     排序其实是一个相当大的概念,主要分为两类:内部排序和外部排序。而我们通常所说的各种排序算法其实指的是内部排序算法。内部排序是基于内存的,整个排序过程都是在内存中完成的,而外部排序指的是由于数据量太大,内存不能完全容纳,排序的时候需要借助外存才能完成(常常是算计着某一部分已经计算过的数据移出内存让另一部分未被计算的数据进入内存)。而我们本篇文章将主要介绍内排序中的几种常用排序算法:

这里写图片描述

还有一个概念问题,排序的稳定性问题。如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的,否则说明该算法不稳定。

二、插入类排序算法
     插入类排序算法的核心思想是,在一个有序的集合中,我们将当前值插入到适合位置上,使得插入结束之后整个集合依然是有序的。那我们接下来就学习下这几种同一类别的不同实现。

     1、直接插入排序
     直接插入排序算法的核心思想是,将第 i 个记录插入到前面 i-1 个已经有序的集合中。下图是一个完整的直接插入排序过程:

这里写图片描述

因为一个元素肯定是有序的,i 等于 2 的时候,将第二个元素插入到前 i-1个有序集合中,当 i 等于3的时候,将第三个元素插入到前 i-1(2)集合中,等等。直到我们去插入最后一个元素的时候,前面的 i-1 个元素构成的集合已经是有序的了,于是我们找到第 i 个元素的合适位置插入即可,整个插入排序完成。下面是具体的实现代码:

public class test3 {

public static void InsertSort(int[] array){
int i=0,j=0,key;
for (i=1;i<10;i++){
key = array[i];
j = i-1;
while(j>=0&&key<array[j]){
//需要移动位置,将较大的值array[j]向后移动一个位置
array[j+1] = array[j];
j--;
}
//循环结束说明找到适当的位置了,是时候插入值了
array[j+1] = key;
}
//输出排序后的数组内容
for (int value : array){
System.out.print(value+",");
}
}
public static void main(String[] args){
//主函数中对其进行调用
int[] array = {1,13,72,9,22,4,6,781,29,2};
InsertSort(array);
}

}

 

 

 

整个程序的逻辑是从数组的第二个元素开始,每个元素都以其前面所有的元素为基本,找到合适的位置进行插入。对于这种按照从小到大的排序原则,程序使用一个临时变量temp保存当前需要插入的元素的值,从前面的子序列的最后一个元素开始,循环的与temp进行比较,一旦发现有大于temp的元素,让它顺序的往后移动一个位置,直到找到一个元素小于temp,那么就找到合适的插入位置了。

因为我们使用的判断条件是,key>array[j]。所以来说,插入排序算法也是稳定的算法。对于值相同的元素并不会更改他们原来的位置顺序。至于该算法的效率,最好的情况是所有元素都已有序,比较次数为n-1,最坏的情况是所有元素都是逆序的,比较次数为(n+2)(n-1)/2,所以该算法的时间复杂度为O(n*n)。

     2、二分折半插入排序
     既然我们每次要插入的序列是有序的,我们完全可以使用二分查找到合适位置再进行插入,这显然要比直接插入的效率要高一些。代码比较类似,不再做解释。

public class test4 {
    public static void halfInsertSort(int[] array){
        for(int k=1;k<array.length;k++){
            int key = array[k];
            //找到合适的位置
            int low,high,mid;
            low = 0;high = k-1;
            while(low <= high){
                mid = (low+high)/2;
                if(key == array[mid])break;
                else if(key > array[mid]){
                    low = mid+1;
                }else{
                    high = mid-1;
                }
            }
            //low的索引位置就是即将插入的位置
            //移动low索引位置后面的所有元素
            for(int x=k-1;x>=low;x--){
                array[x+1] = array[x];
            }
            array[low] = key;
        }
        //遍历输出有序队列内容
        for(int key:array){
            System.out.print(key + ",");
        }
    }
    public static void main(String[] args){
        int[] array = {1,13,72,9,22,4,6,781,29,2};
        halfInsertSort(array);
    }

}

 

 

 

虽然,折半插入改善了查找插入位置的比较次数,但是移动的时间耗费并没有得到改善,所以效率上优秀的量可观,时间复杂度仍然为O(n*n)。

     2、希尔排序
     直接插入排序在整个待排序序列基本有序的情况下,效率最佳,但我们往往不能保证每次待排序的序列都是基本有序的。希尔排序就是基于这样的情形,它将待排序序列拆分成多个子序列,保证每个子序列的组成元素相对较少,然后通过对子序列使用直接排序。对于本就容量不大的子序列来说,直接排序的效率是相当优秀的。

希尔排序算法使用一个距离增量来切分子序列,例如:

这里写图片描述

如图,我们初始有一个序列,按照距离增量为4来拆分的话,可以将整个序列拆分成四个子序列,我们对四个子序列内部进行直接插入排序得到结果如下:

这里写图片描述

修改距离增量重新划分子序列:

这里写图片描述

很显然,当距离增量变小的时候,序列的个数也会变少,但是这些子序列的内部都基本有序,当对他们进行直接插入排序的时候会使得效率变高。一旦距离增量减少为1,那么子序列的个数也将减少为1,也就是我们的原序列,而此时的序列内部基本有序,最后执行一次直接插入排序完成整个排序操作。

下面我们看算法是的具体实现:

public class test4 {
    /*渐减delete的值*/
    public static void ShellSort(){
        int[] array = {46,55,13,42,94,17,5,70};
        int[] delets = {4,2,1};
        for (int i=0;i<delets.length;i++){
            oneShellSort(array,delets[i]);
        }
        //遍历输出数组内容
        for(int value : array){
            System.out.print(value + ",");
        }
    }
    /*根据距离增量的值划分子序列并对子序列内部进行直接插入排序*/
    public static void oneShellSort(int[] array,int delet){
        int temp;
        for(int i=delet;i<array.length;i++){
            //从第二个子序列开始交替进行直接的插入排序
            //将当前元素插入到前面的有序队列中
            if(array[i-delet] > array[i]){
                temp = array[i];
                int j=i-delet;
                while(j>=0 && array[j] > temp){
                    array[j+delet] = array[j];
                    j -= delet;
                }
                array[j + delet] = temp;
            }
        }
    }
    public static void main(String[] args){
        ShellSort();
    }

}

三、交换类排序
     交换类的排序算法一般是利用两个元素之间的值的大小进行比较运算,然后移动外置实现的,这类排序算法主要有两种:
     1、冒泡排序
     冒泡排序通过两两比较,每次将最大或者最小的元素移动到整个序列的一端。这种排序相当常见,也比较简单,直接上代码:

public class test4 {
    public static void bubbleSort(int[] array){
        int temp = 0;
        for(int i=0;i<array.length-1;i++){
            for(int j =0;j<array.length-1-i;j++){
                if(array[j]>array[j+1]){
                    //交换两个数组元素的值
                    temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
            }
        }
        //遍历输出数组元素
        for(int value : array){
            System.out.print(value + ",");
        }
    }
    public static void main(String[] args){
        int[] array={2,1,55,66,44,22,99,101,100};
        bubbleSort(array);
    }

}

 2、快速排序

有没有既不浪费空间又可以快一点的排序算法呢?那就是“快速排序”啦!光听这个名字是不是就觉得很高端呢。

假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:

3 1 2 5 4 6 9 7 10 8

在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想一想,你有办法可以做到这点吗?

 

方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。

这里写图片描述
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
这里写图片描述
这里写图片描述
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:

6 1 2 5 9 3 4 7 10 8
这里写图片描述
这里写图片描述
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:

6 1 2 5 4 3 9 7 10 8

第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:

3 1 2 5 4 6 9 7 10 8

这里写图片描述
这里写图片描述
这里写图片描述

到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。

OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。

左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧

如果你模拟的没有错,调整完毕之后的序列的顺序应该是:

2 1 3 5 4

OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下:

1 2 3 4 5 6 9 7 10 8

对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下

1 2 3 4 5 6 7 8 9 10

到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面上个霸气的图来描述下整个算法的处理过程。
这里写图片描述

这是为什么呢?

快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。我们后面还会遇到“二分”思想,到时候再聊。先上代码,如下

public class test4 {
    public static void quickSort(int[] arr,int low,int high){
        int i,j,temp,t;
        if(low>high){
            return;
        }
        i=low;
        j=high;
        //temp就是基准位
        temp = arr[low];

        while (i<j) {
            //先看右边,依次往左递减
            while (temp<=arr[j]&&i<j) {
                j--;
            }
            //再看左边,依次往右递增
            while (temp>=arr[i]&&i<j) {
                i++;
            }
            //如果满足条件则交换
            if (i<j) {
                t = arr[j];
                arr[j] = arr[i];
                arr[i] = t;
            }

        }
        //最后将基准为与i和j相等位置的数字交换
        arr[low] = arr[i];
        arr[i] = temp;
        //递归调用左半数组
        quickSort(arr, low, j-1);
        //递归调用右半数组
        quickSort(arr, j+1, high);
    }


    public static void main(String[] args){
        int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
        quickSort(arr, 0, arr.length-1);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }


}

四、选择类排序
     选择类排序的基本思想是,每一趟会在n个元素中比较n-1次,选择出最大或者最小的一个元素放在整个序列的端点处。选择类排序有基于树的也有基于线性表的,有关树结构的各种排序算法,我们将在后续文章中进行描述,此处我们实现简单的选择排序算法。

public class test4 {
    public static void ChooseSort(int[] array){
        for (int i=0;i<array.length;i++){
            for (int j=i+1;j<array.length;j++){
                if(array[i]>array[j]){
                    //发现比自己小的元素,则交换位置
                    int temp = array[j];
                    array[j]=array[i];
                    array[i] = temp;
                }
            }
        }
        //输出排序后的数组内容
        for (int key  : array){
            System.out.print(key+",");
        }
    }


    public static void main(String[] args){
        int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
        ChooseSort(arr);

    }


}

五、归并类排序算法
     这里的归并类排序算法指的就是归并排序。归并排序的核心思想是,对于一个初始的序列不断递归,直到子序列中的元素足够少时,对他们进行直接排序。然后递归返回继续对两个分别有序的序列进行直接排序,最终递归结束的时候,整个序列必然是有序的。

这里写图片描述

对于一个初始序列,我们递归拆分的结果如上图。最小的子序列只有两个元素,我们可以轻易的对他们进行直接的排序。简单的排序结果如下:

这里写图片描述

然后我们递归返回:

这里写图片描述

初看起来和我们的希尔排序的基本思想有点像,希尔排序通过对初始序列的稀疏化,使得每个子序列在内部上都是有序的,最终在对整个序列进行排序的时候,序列的内部基本有序,总体上能提高效率。但是我们的归并排序的和核心思想是,通过不断的递归,直到子序列元素足够少,在内部对他们进行直接的排序操作,当递归返回的时候,对返回的两个子表再次进行归并排序,使得合成的新序列是有序的,一直到递归返回调用结束时候,整个序列就是有序的。

 

 

import java.util.Arrays;

public class test4 {

    //归并排序
    /*归并排序采用递归实现
     * 分阶段可以理解为就是递归拆分子序列的过程、
     * 治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],
     * */
    public static void main(String []args){
        int []arr = {9,8,7,6,5,4,3,2,1};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void sort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(arr,0,arr.length-1,temp);
    }
    private static void sort(int[] arr,int left,int right,int []temp){
        if(left<right){
            int mid = (left+right)/2;
            sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){
        int i = left;//左序列指针
        int j = mid+1;//右序列指针
        int t = 0;//临时数组指针
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//将左边剩余元素填充进temp中
            temp[t++] = arr[i++];
        }
        while(j<=right){//将右序列剩余元素填充进temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){
            arr[left++] = temp[t++];
        }
    }
}

 

 
posted @ 2020-01-06 15:39  FYX扛把子  阅读(...)  评论(...编辑  收藏