十大排序算法分析与总结

@[TOC] 排序算法

分类

  • 线性时间比较类排序:通过比较来决定元素的相对次序,由于时间复杂度不能突破 0 ( n l o g n ) 0(nlogn) 0(nlogn) ,因此称为线性时间比较类排序。其类别:
  • 线性时间费比较类排序:不通过比较决定来决定元素的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
    具体比较为:
    非线性时间比较类排序
    交换排序:冒泡排序、快速排序
    插入排序:简单插入排序、希尔排序
    选择排序:简单选择排序、堆排序
    归并排序:二路归并排序、多路归并排序
    线性时间比较类排序
    基数排序
    桶排序

术语说明

  • 稳定:若 a a a原本在 b b b前面,而 a = = b a==b a==b的情况下进行排序,排序之后 a a a仍然在 b b b前面。
  • 不稳定:若 a a a原本在 b b b前面,而 a = = b a==b a==b的情况下进行排序,排序之后 a a a b b b后面。
  • 内排序:所有排序操作数都在内存中完成
  • 外排序:数据量太大,因此把数据放在磁盘中,而排序通过磁盘和内存的传输才能进行
  • 算法衡量标准:时间复杂度(一个算法执行所消耗的时间)、空间复杂度(运行完一个程序所需要的内存大小)

算法的详解

1、交换排序

1.1 冒泡排序(稳定排序)

思想: 通过相邻记录的两两比较和交换,使关键字较小的记录像水中的气泡一样逐趟向上漂浮,而关键字较大的记录好比石块一样下下沉,每一趟有一块“最大”的石头沉到水底。
步骤:
1. 将第一条记录的关键字和第二条记录关键字比较,若第一个关键字 ≥ \ge 第二个关键字,则交换两条记录;然后比较第二条和第三条记录,直至最后一个关键字;
2. 按照步骤1即可完成一趟排序,其结果是最大的关键字排在最后。接着进行第二趟排序,总共需要排序的元素为 n − 1 n-1 n1
3. 按照2即可完成排序,若有 n n n个元素,则需要排序 n − 1 n-1 n1趟。
代码:

// 1) 冒泡排序: 稳定排序
    //        -- 时间复杂度:最好0(N),最差:0(N^2)---->平均:O(N^2)
    //        -- 空间复杂度:O(1)
    //        -- 稳定排序
    int [] bubleSort(int primitives[]) {
        // 数组长度小于或者等于1
        if (primitives.length <= 1)
            return primitives;
        // 总的循环趟数,假设数组元素为n,则loop = n-1
        for (int loop = 0; loop < primitives.length - 1; loop++) {
            boolean isSequence = true; // 增加标记位,若某一趟发现待排序的数组已经完全有序了,则无需排序直接退出即可。
            // 按照冒泡思想,每一趟最大值排在最后面,因此在第i趟,最后的n-loop后面元素无需比较,已经有序.因此比较的次数为:n-loop-1
            for (int i = 0; i < primitives.length - loop - 1; i++) {
                if (primitives[i] > primitives[i + 1]) { // 稳定排序,相等不改变与原顺序
                    // 使用变量进行两个元素的交换
//                    int temp = primitives[i];
//                    primitives[i] = primitives[i+1];
//                    primitives[i+1] = temp;
                    // 位运算交换数据
                    primitives[i] = primitives[i] ^ primitives[i + 1];
                    primitives[i + 1] = primitives[i] ^ primitives[i + 1];
                    primitives[i] = primitives[i] ^ primitives[i + 1];
                    isSequence = false;
                }
            }
            if(isSequence)
                break;
        }
        return primitives;
    }
1.2 快速排序(import)

思想: 是对冒泡排序算法的一种改进。我们从数组中选择一个元素,把这个元素称之为 中轴元素 吧,然后把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,我们无需再移动中轴元素的位置。在进行完第一轮后,再按照同样的思想对左右两部分的元素进行同样的操作。
步骤:设待排序记录存于 r [ t ] r[t] r[t], r [ t + 1 ] r[t+1] r[t+1],…, r [ w ] r[w] r[w]中。
1). 初始化两个变量 i = t i=t i=t j = w j=w j=w,选择基准值 p r i v o t = r [ t ] privot = r[t] privot=r[t]
2). 先从最右边 j = w j=w j=w开始逐个和基准值进行比较,若发现 r [ j ] r[j] r[j]小于 p r i v o t privot privot则说明需要交换两者位置,此时将 r [ i ] = r [ j ] r[i] = r[j] r[i]=r[j], i++;
3). 继第2)步后,开始从 i i i的位置逐个向后扫描,直至r[i] 大于 p r i v o t privot privot, 则说明需要交换位置,将 r [ j ] = r [ i ] r[j] = r[i] r[j]=r[i]且j++;
4) . 如此重复2)和3)步,最终当 l o w > = h i g h low >= high low>=high时,完成一趟排列,最终赋值:r[low] = privot。
5) . 经过第4)步即可将一个序列分成左右两部分,按照递归处理左右两部分。
代码

// 2)快速排序
    void quickSort(int prim[], int low, int high){
        // 左索引 小于 右索引
        if(low < high ) {
            int baseIndex = partition(prim,low,high);
            quickSort(prim,low,baseIndex-1);
            quickSort(prim,baseIndex+1,high);
        }
    }

    /**
     * 进行完一轮排序,并将数组分成两部分,将基准索引返回,供下趟排序使用
     * @describe:具体每一轮的实现,将low位置对应的值作为基值,最终分成两部分,作部分小于基值右部分大于基值
     * @param prim
     * @param low
     * @param high
     * @return
     */
    private int partition(int[] prim, int low, int high) {
        int privot = prim[low]; // 将数组中第一个元素作为基准值,标记基准值
        /**
         * 值得注意的是:在进行比较时,不要每一步去交换基准值,因为基准值最终所在的位置一直在调整,没有固定
         * 一定的位置,因此避免交换次数过多,不要交换基准值,优化算法如下:
         */
        while(low < high) { // 两个索引不一致,需要调整
            // 若右部high索引对应的value小于或者等于左部low索引对应的值,则逐步减
           while(low < high && prim[high] >= privot) high--;
            // 直至 到了 high对应的value值小于low对应的value值,此时将high对应的值赋值给low位置
            prim[low] = prim[high];
            // 经过上一步,除了low==high,那一定有prim[low]<privator,因此low逐步增加
            while(low < high && prim[low] <= privot) low++;
            // 直至 带了low对应的值大于high对应的值,则此时将low对应的值赋值给high对应的位置
            prim[high] = prim[low];
        }
        if(low != high)
            System.out.println("Error, the value of low is not equal with the hign value");
        prim[low] = privot;
        return low;
    }

分析: 从算法可知,快速排序属于不稳定排序。
-时间复杂度: O(nlogn)
-空间复杂度: O(logn)

2、选择排序

2.1 简单选择排序

**思想:**每次从待排序列中选择最小(最大,看排列方式)关键字,顺序放在已经排好的序列中,直到序列已经全部有序。
**步骤: **
1)查找待排序列中最小的关键字,并将它和该区间的第一个关键字交换位置;(一层循环,找最小关键字)
2)重复 n − 1 n-1 n1次1)步骤,直至序列已经有序。(二层循环,需要的循环趟数)
代码:

 //  1)  简单选择排序:
              //  a) n-1趟
              //  b) 每一趟选择最小的元素并与无序部分第一个元素进行交换
      public void simpleSelectSort(int [] prim) {
          int len = prim.length;
          if(len < 2)
              return;
          int min_index = 0;
          for(int i = 0; i < len -1 ; i++) { // 循环的趟数,每一趟确定i位置的元素
              min_index = i; // 默认无序部分第一个元素是最小元素
              for(int j = i +1; j<len; j++){
                  if(prim[j] < prim[min_index])
                      min_index = j; // 更新最小值索引
              }
              // 这一趟已找出最小值,因此进行i位置与min_index的值交换
              int temp = prim[i];
              prim[i] = prim[min_index];
              prim[min_index] = temp;
          }
       }

分析:不稳定
- 时间复杂度: O ( N 2 ) O(N_2) O(N2) 最好最坏都是如此
- 空间复杂度: O ( 1 ) O(1) O(1)

2.2 堆排序

**思想:**主要是借助二叉树的结构调整,分为以下两步:构建堆(大根堆、小根堆)、交换首尾两元素继续讲剩余的元素构建堆
步骤:
1)构建堆(大根堆为例):根节点值大于左右节点值。基于层次遍历与数组对应索引关系,当根节点值索引为 i i i时,左右节点值
的索引分别为: 2 ∗ i + 1 2*i +1 2i+1 2 ∗ i + 2 2*i+2 2i+2。每次挑选最大的值放在根节点,循环 l e n / 2 len/2 len/2次。
2)交换再调整堆:交换根节点值和最后元素的值,再按照 1 ) 1) 1)中的步骤调整成大根堆,循环len-1次。
代码:

/**
     *  1) 堆排序:
     *      第一步:构成堆(大根堆、小根堆)。
     *      第二步:将根元素与最后一个元素交换,再将剩余数据调整成堆
     *      大根堆:根节点值大于左右节点值(适合升序排序)、小根堆:根节点值小于左右节点值(适合降序排序)
     *   因此代码实现主要分为两部分:构堆、交换再构堆
     *   a) 构堆:由于调整堆是二叉树,因此调整堆得轮数:len/2-1。假设根节点编号为i,则左节点编号2*i+1,右节点2*i+2
     *            从根、左右节点选择最大的节点放到根节点上。
     */
     public void heapSelectSort(int [] prim){
         int len = prim.length;
         if(len < 2)
             return;
         // 通过数组进行构建堆、大根堆(升序的方式)
         for(int i = len/2-1; i>=0; i--) {
            constructBigHeap(prim, i,len);
         }
         // 此时prim的数组满足大根堆要求,即可以用它与最后一个元素进行交换
         for(int j= len-1; j>0; j--) {
             swap(prim, 0, j);
             constructBigHeap(prim,0,j);
         }
     }

    /**
     * @descri 交换数据
     * @param prim
     * @param i
     * @param j
     */
    private void swap(int[] prim, int i, int j) {
        int temp = prim[i];
        prim[i] = prim[j];
        prim[j] = temp;
    }

    /**
     * @descri 构建大根堆
     * @param prim: 待构建的数组
     * @param i:i表示当前的根节点
     * @param len:len表示prim数组待调整的长度
     */
    private void constructBigHeap(int[] prim, int i,int len) {
        int temp = prim[i]; // 记住当前的根节点值
        for(int k = 2*i+1; k<len; k=2*k+1){
            if(k+1 < len && prim[k] < prim[k+1]) // 比较左右节点,选组最大值
                k++; // k 记住左右节点最大值索引
            if(temp < prim[k]) { // 在与根节点进行比较,挑选根、左右节点最大值
                prim[i] = prim[k];
                i = k; // 更换根节点位置
            }
            else
                break; // 说明该数组已经满足大根堆,无需调整
        }
        prim[i] = temp;
    }

分析:不稳定
- 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)最好、最坏也是如此.
- 空间复杂度: O ( 1 ) O(1) O(1)

3、归并排序

3.1 归并排序

思想: 从上至下递归拆分、从下至上递归返回合并。
步骤:
1)递归拆分:先把待排序数组分为左右两个子序列,再讲子序列左右划分为4个子序列,直至最小序列的个数为1。
2)层级合并:从下到上层级合并,也可以理解递归层级返回。先作部分排序合并,再右部分排序合并,最终左右部分排序合并,直至最顶层。
代码:

 /**
     * @describe: 归并排序
     * @param prim 待拆分数组
     * @param left 待拆分数组最小下标
     * @param right 待拆分数组最大下标
     */
      public  void mergeSort(int prim[], int left, int right) {

          int mid = (left + right) / 2; // 中间位置索引
          if(left < right) {  // 递归终止条件 left >= right
              mergeSort(prim, left, mid); // 递归拆分左边
              mergeSort(prim, mid+1, right); // 递归拆分右边
              mergeProcess(prim, left, mid, right); // 合并左右
          }
      }

    /**
     * @describe: 对左右两部分进行按照从小到大的顺序排序,即合并左右两部分
     * @param prim 待合并的数组
     * @param left 待合并数组左部分的最小索引值
     * @param mid   待合并数组中间位置的索引
     * @param right 待合并数组右部分做大索引值
     */
    private void mergeProcess(int[] prim, int left, int mid, int right) {
        int [] temp = new int[right - left + 1]; // 临时数组,用于保存每次合并之后的结果
        int i = left; // 左部分第一个元素的索引
        int j = mid + 1; //右部分第一个元素的索引
        int k = 0;   // 临时数组的索引
        while(i <= mid && j <= right) {  // 这个循环先排序出左右部分合并后较小序列,结束时肯定有一部分已经没有待排序的数
            if(prim[i] <= prim[j])
                temp[k++] = prim[i++];
            else
                temp[k++] = prim[j++];
        }
        // 至此,至少有一部分元素已经全部在temp中即:i> mid 或者 j>right
        while(i <= mid) { // j > right
            temp[k++] = prim[i++];
        }
        while(j <= right) {
            temp[k++] = prim[j++];
        }
        // 将临时数组重新复制到prim数组中
        for(int n =0; n<temp.length; n++)
            prim[left + n] = temp[n];
    }

分析:稳定
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度: O ( n ) O(n) O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值