Java基础语法——三大基础排序

每日正能量

成年人的世界,似乎只有赚到足够的钱,才能过上简单、安逸、自由的生活,才能让自己活得更有底气些。所以,多一些努力吧,少点功夫矫情。

1. 排序的定义

对一序列对象根据某个关键字进行排序。

1.1 术语说明

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;

  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

  • 内排序:所有排序操作都在内存中完成;

  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;

  • 时间复杂度: 一个算法执行所耗费的时间。

  • 空间复杂度:运行完一个程序所需内存的大小。

1.2 算法总结

图片名词解释:

  • n: 数据规模

  • k: “桶”的个数

  • In-place: 占用常数内存,不占用额外内存

  • Out-place: 占用额外内存

  • 以上表格是基于数组进行排序的一般性结论

1.3 算法分类

 

1.4 比较和非比较的区别

常见的选择排序、插入排序、快速排序、希尔排序、归并排序、堆排序、冒泡排序等属于比较排序在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。 比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。 非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

2. 冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

2.1 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;

  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

  • 针对所有的元素重复以上的步骤,除了最后一个;

  • 重复步骤1~3,直到排序完成。

2.2 动图演示

 

2.3 代码实现

/**
       * 冒泡排序
       *
       * @param array
       * @return
       */
      public static int[] bubbleSort(int[] array) {
         if (array.length == 0)
              return array;
         for (int i = 0; i < array.length; i++)
             for (int j = 0; j < array.length - 1 - i; j++)
                 if (array[j + 1] < array[j]) {
                     int temp = array[j + 1];
                     array[j + 1] = array[j];
                     array[j] = temp;
                 }
         return array;
     }

2.4 冒泡排序的优化

原始的冒泡排序有哪些可以优化的点呢?

让我们回顾一下刚才描述的排序细节,仍然以{5,8,6,3,9,2,1,7}这个数列为例,当排序算法分别执行到第6、第7轮时,数列状态如下。

 

很明显可以看出,经过第6轮排序后,整个数列已然是有序的了。可是排序算法仍然兢兢业业地继续执行了第7轮排序。

在这种情况下,如果能判断出数列已经有序,并做出标记,那么剩下的几轮排序就不必执行了,可以提前结束工作。

冒泡排序第2版代码示例如下:

public static void sort(int array[]) { 
     for(int i = 0; i < array.length - 1; i++) { 
         //有序标记,每一轮的初始值都是true
         boolean isSorted = true; 
         for(int j = 0; j < array.length - i - 1; j++)  { 
             int tmp = 0;
             if(array[j] > array[j+1])  { 
                 tmp = array[j];
                 array[j] = array[j+1]; 
                 array[j+1] = tmp; 
                 //因为有元素进行交换,所以不是有序的,标记变为false 
                 isSorted = false;
             } 
         } 
         if(isSorted){ 
             break; 
         }
     } 
 } 
public static void main(String[] args){ 
    int[] array = new int[]{5,8,6,3,9,2,1,7}; 
    sort(array); 
    System.out.println(Arrays.toString(array)); 
}

与第1版代码相比,第2版代码做了小小的改动,利用布尔变量

isSorted作为标记。如果在本轮排序中,元素有交换,则说明数列无

序;如果没有元素交换,则说明数列已然有序,然后直接跳出大循环。

 

为了说明问题,这次以一个新的数列为例。

 

这个数列的特点是前半部分的元素(3、4、2、1)无序,后半部分的元素(5、6、7、8)按升序排列,并且后半部分元素中的最小值也大于前半部分元素的最大值。

下面按照冒泡排序的思路来进行排序,看一看具体效果。

元素4和5比较,发现4小于5,所以位置不变。

元素5和6比较,发现5小于6,所以位置不变。

元素6和7比较,发现6小于7,所以位置不变。

元素7和8比较,发现7小于8,所以位置不变。

第1轮结束,数列有序区包含1个元素。

 

第2轮

元素3和2比较,发现3大于2,所以3和2交换。

 

元素3和4比较,发现3小于4,所以位置不变。

元素4和5比较,发现4小于5,所以位置不变。

元素5和6比较,发现5小于6,所位位置不变。

元素6和7比较,发现6小于7,所以位置不变。

元素7和8比较,发现7小于8,所以位置不变。

第2轮结束,数列有序区包含2个元素。

 

同学,你发现其中的问题了吗?

其实右边的许多元素已经是有序的了,可是每一轮还是白白的比较了许多次。

这正式冒泡排序中另一个需要优化的点。

这个问题的关键点在于对数列有序区的界定。

按照现有的逻辑,有序区的长度和排序的轮数是相等的。例如第1轮排序过后的有序区长度是1,第2轮排序过后的有序区长度是2 ……

实际上,数列真正的有序区可能会大于这个长度,如上述例子中在第2轮排序时,后面的5个元素实际上都已经属于有序区了。因此后面的多次元素比较是没有意义的。

那么,该如何避免这种情况呢?我们可以在每一轮排序后,记录下来最后一次元素交换的位置,该位置即为无序数列的边界,再往后就是有序区了。

冒泡排序第3版代码示例如下:

public static void sort(int array[]) { 
     //记录最后一次交换的位置 
     int lastExchangeIndex = 0; //无序数列的边界,每次比较只需要比到这里为止 
     int sortBorder = array.length - 1; 
     for(int i = 0; i < array.length - 1; i++){ 
        //有序标记,每一轮的初始值都是true 
        boolean isSorted = true; 
        for(int j = 0; j < sortBorder; j++) { 
            int tmp = 0; 
            if(array[j] > array[j+1]) { 
                tmp = array[j]; 
                array[j] = array[j+1]; 
                array[j+1] = tmp; 
                // 因为有元素进行交换,所以不是有序的,标记变为false 
                isSorted = false; 
                // 更新为最后一次交换元素的位置 
                lastExchangeIndex = j; 
            } 
        } 
        sortBorder = lastExchangeIndex; 
        if(isSorted){ 
             break; 
         } 
      } 
 } 
 
 public static void main(String[] args){ 
    int[] array = new int[]{3,4,2,1,5,6,7,8}; 
    sort(array); 
     System.out.println(Arrays.toString(array)); 
 }

在第3版代码中,sortBorder就是无序数列的边界。在每一轮排序过程中,处于sortBorder之后的元素就不需要再进行比较了,肯定是有序的。

2.5 算法分析

最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)

3. 选择排序(Selection Sort)

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

3.1 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;

  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

  • n-1趟结束,数组有序化了。

3.2 动图演示

  

 

3.3 代码实现

/**
     * 选择排序
     * @param array
     * @return
     */
    public static int[] selectionSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) //找到最小的数
                    minIndex = j; //将最小数的索引保存
            }
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }
        return array;
    }

3.4 算法分析

最佳情况:T(n) = O(n2) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)

4. 插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

4.1 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;

  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;

  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;

  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

  • 将新元素插入到该位置后;

  • 重复步骤2~5。

4.2 动图演示

4.3 代码实现

/**
     * 插入排序
     * @param array
     * @return
     */
    public static int[] insertionSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 1; i < array.length; i++) {
            //外层循环,从第二个开始比较
            for (int j = i; j > 0; j--) {
                //内层循环,与前面排好序的数据比较,如果后面的数据小于前面的则交换
                if (array[j] < array[j - 1]) {
                    int temp = array[j - 1];
                    array[j - 1] = array[j];
                    array[j] = temp;
                } else {
                    //如果不小于,说明插入完毕,退出内层循环
                    break;
                }
            }
        }
        return array;
}

4.4 算法分析

最佳情况:T(n) = O(n) 最坏情况:T(n) = O(n2) 平均情况:T(n) = O(n2)

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值