数据结构基本八大排序算法

1.排序算法简介

  由于待排序的记录数量不同,使得排序过程中涉及的存储器不同,可以将排序方法分为两大类:内部排序外部排序
  内部排序:待排序记录存放在计算机的随机存储器(内存)中进行的排序过程。内部排序的方法很多,但就其全面性能而言,很难提出一种被认为最好的方法。每一种方法都有自己的优缺点,适合在不同的环境下使用(比如记录的初始排列状态等)。按照排序过程中依据的不同原则对内部排序大概可以分为以下几类:插入排序,交换排序,选择排序,归并排序和基数排序
  外部排序:待排序的记录数量很大,内存一次不能容纳全部记录,在排序过程中需要对外存进行访问的排序过程。(内存和外存同时使用)
  各种排序算法之间的关系罗列:
  排序算法关系图

2.各种排序算法介绍与实例

2.1直接插入排序

  算法概念:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表任然有序。
  算法思想:假设待排序的记录存放在数组R[1..n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。从i=2起直至i=n为止,依次将R[i]插入当前的有序区R[1..i-1]中,生成含n个记录的有序区。
  算法示例

//(直接)插入排序,时间复杂度O(n^2),稳定
public class InsertSort {

    public static void main(String[] args) {
        int[] nums = {5,3,2,7,1,4,6};
        startSort(nums);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

    public static void startSort(int[] nums) {
        for (int i = 0; i < nums.length - 1; i++) {
            int j = i + 1;
            //temp中存放需要插入的元素
            int temp = nums[j];
            //temp为待插入的元素,和已经有序列表的最后一个数(最大数)进行比较
            //若max>temp,max向后移动,temp再和前面的次最大数比较...
            //若max<temp,temp直接插入在最后即可
            while(i >= 0 && nums[i] > temp) {
                nums[j] = nums[i];
                i --;
                j --;
            }
            nums[j] = temp;
        }
    }

}

  算法总结:时间复杂度O(n^2),是稳定的排序方式。

2.2希尔排序(缩小增量排序)

  算法概念:也是一种插入排序的算法,但在时间效率上有较大的改进。
  算法思想
  (a)先将整个待排序记录分割成若干个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
  (b)先取一个小于n(n为数组长度)的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2(其中d2小于d1)重复上述的分组和排序,直至所取的增量dt=1(dt<dt-1<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
  算法示例

//希尔排序,时间复杂度O(nlogn),不稳定
public class ShellSort {

    public static void main(String[] args) {
        int[] nums = {5,3,2,7,1,4,6};
        startSort(nums);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

    public static void startSort(int[] nums) {
        // 分成2组
        int len = nums.length / 2;
        while (len >= 1) {
            for (int i = 0; i < len; i++) {
                // 使用直接插入排序对分组进行排序
                for (int j = i; j < nums.length - len; j += len) {
                    int k = j + len;
                    int temp = nums[k];
                    while (j >= 0 && nums[j] > temp) {
                        nums[k] = nums[j];
                        j -= len;
                        k -= len;
                    }
                    nums[k] = temp;
                }
            }
            len /= 2;
        }
    }
}

  算法总结:时间复杂度O(nlogn),不稳定。

2.3简单选择排序

  算法思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数中再找出最小的数与第二个位置的数交换,如此循环到倒数第二个数,将该数与最后一个数比较,较小的一个放在前一个位置。
  算法示例

//选择排序,时间复杂度O(n^2),不稳定
public class SelectSort {

    public static void main(String[] args) {
        int[] nums = { 5, 3, 2, 7, 1, 4, 6 };
        startSort(nums);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

    public static void startSort(int[] nums) {
        // 标志位
        int position = 0;
        for (int i = 0; i < nums.length; i++) {
            int j = i + 1;
            position = i;
            int temp = nums[i];
            // 找出后面数中的最小者
            for (; j < nums.length; j++) {
                if (nums[j] < temp) {
                    temp = nums[j];
                    position = j;
                }
            }
            nums[position] = nums[i];
            nums[i] = temp;
        }
    }
}

  算法总结:时间复杂度O(n^2),不稳定。

2.4堆排序

  算法思想:堆排序是一种用完全二叉树解决问题的高效算法,合法的最大堆就是每一个节点值都要大于或等于它的孩子节点,在数组中可表示为:(i从0开始)arrays[i]>=arrays[2*i+1] && arrays[i]>=arrays[2*i+2],最小堆的概念和最大堆相反,此处采用最大堆。
  堆排序树的构造过程找最大值过程由下图,数组arrays[0….n]为:17,8,45,84,2,94,刚找到最大值后把最大值即94放在数组的最后面arrays[n],然后进入递归把arrays[0…n-1]再进入下面图这个过程,只是把排好序的最大值不放入到这个过程中,就这样把值一个个的冒出来,找到最大值后把这个最大值放到数组的最后面,进入下一个递归。
  注意:每次都要扫描整个数组,最先发现不符合最大堆的先调整。
  堆排序示意图1
  堆排序示意图2
  算法示例

//堆排序,时间复杂度O(nlogn),不稳定
public class HeapSort {

    public static void main(String[] args) {
        int[] nums = {5,3,2,7,1,4,6};
        int e = nums.length - 1;
        nums = startSort(nums,e);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

    // 堆排序
    public static int[] startSort(int[] nums, int e) {
        // 堆中的元素多于1个
        if (e > 0) {
            initHeap(nums, e);// 初始化堆
            // 在一轮的堆排序中,找到了最大值,将最大值与最后一个元素交换,重新开始下一轮
            nums[0] = nums[e] + nums[0];
            nums[e] = nums[0] - nums[e];
            nums[0] = nums[0] - nums[e];
            // 递归开始下一轮
            startSort(nums, e - 1);
        } else {
            // 堆中不多于1个元素,不需要进行堆排序
            return nums;
        }
        return nums;
    }

    // 初始化堆
    public static void initHeap(int[] nums, int e) {
        int m = (e + 1) / 2; // 父节点的个数
        for (int i = 0; i < m; i++) {
            boolean flag = buildHeap(nums, e, i);
            // 如果父节点和孩子节点之间有交换,需要重新扫描整个树中的父节点
            if (flag) {
                i = -1;
            }
        }
    }

    // 建立堆
    public static boolean buildHeap(int[] nums, int e, int i) {
        int l_child = 2 * i + 1;// 左孩子
        int r_child = 2 * i + 2;// 右孩子
        // 没有右孩子的时候只需要比较父节点和左孩子节点的大小
        if (r_child > e) {
            // 父节点比左孩子节点小,需要交换
            if (nums[i] < nums[l_child]) {
                // 不使用中间变量交换父节点和左孩子节点的值
                nums[i] = nums[i] + nums[l_child];
                nums[l_child] = nums[i] - nums[l_child];
                nums[i] = nums[i] - nums[l_child];
                return true;
            } else {
                return false;
            }
        }
        // 同时拥有左右两个孩子节点的情况,在三者中找出最大值进行交换
        if (nums[i] < nums[l_child]) {
            // 父节点比左孩子节点小
            if (nums[l_child] > nums[r_child]) {
                // 左孩子节点是三者中最大的,和父节点交换
                nums[i] = nums[i] + nums[l_child];
                nums[l_child] = nums[i] - nums[l_child];
                nums[i] = nums[i] - nums[l_child];
                return true;
            } else {
                // 右孩子节点是三者中最大的,和父节点交换
                nums[i] = nums[i] + nums[r_child];
                nums[r_child] = nums[i] - nums[r_child];
                nums[i] = nums[i] - nums[r_child];
                return true;
            }
        } else if (nums[i] < nums[r_child]) {
            // 父节点比右孩子节点小
            // 右孩子节点是三者中最大的,和父节点交换
            nums[i] = nums[i] + nums[r_child];
            nums[r_child] = nums[i] - nums[r_child];
            nums[i] = nums[i] - nums[r_child];
            return true;
        }
        // 父节点最大,不需要交换
        return false;
    }

}

  算法总结:时间复杂度O(nlogn),不稳定。

2.5冒泡排序

  算法思想:在要排序的一组数中,将第一个记录的关键字与第二个记录的关键字进行比较,让大的往下沉,小的往上冒,然后再比较第二个和第三个,以此类推。比较完一趟,最大的那个已经放到了最后的位置,这样可以对剩下的n-1个再循环比较。
  冒泡排序示意图
  算法示例

//冒泡排序,时间复杂度O(n^2),稳定
public class BubbleSort {

    public static void main(String[] args) {
        int[] nums = {5,3,2,7,1,4,6};
        startSort(nums);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

    public static void startSort(int[] nums) {
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = 0; j < nums.length - 1 - i; j++) {
                if (nums[j] > nums[j + 1]) {
                    int temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                }
            }
        }
    }

}

  算法总结:算法复杂度为O(n^2),稳定的。

2.6快速排序

  算法思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,这里选择第一个元素为基准。一个指向基准元素且整个过程指针不变,一个指向最后一个元素,两者比较,前面的元素大于后面的元素则交换,非基准元素的指针向中心移动一位,继续上述步骤,通过一趟扫描,将待排序列分成两个部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素的位置是排好序后的正确位置,然后再用同样的方法递归地排序两个部分。
  快速排序是对冒泡排序的一种改进。
  快速排序示意图
  算法示例

//快速排序,时间复杂度O(nlogn),不稳定
public class QuickSort {

    public static void main(String[] args) {
        int[] nums = {5,3,2,7,1,4,6};
        int low = 0;
        int high = nums.length - 1;
        nums = startSort(nums,low,high);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

     public static int[] startSort(int[] nums,int low,int high) {
          //递归的终止条件是low >= high
          if(low < high) {
           int middle = getMiddle(nums,low,high);
           //对基准元素前面部分递归排序
           startSort(nums,low,middle-1);
           //对基准元素后面部分递归排序
           startSort(nums,middle+1,high);
          }
          return nums;
         }

         public static int getMiddle(int[] nums, int low,int high) {
          //选择基准元素
          int temp = nums[low];
          //两个指针都向中心处移动,直至相等
          while (low < high) {
           //右指针的元素总是比基准元素大时
           while (low < high && nums[high] >= temp) {
            high --;
           }
           //比基准元素小的移动到左侧
           nums[low] = nums[high];
           //左指针的元素总是比基准元素小时
           while (low <high && nums[low] <= temp) {
            low ++;
           }
           //比基准元素大的移动到右侧
           nums[high] = nums[low];
          }
          nums[low] = temp;
          return low;
         }

}

  算法总结:时间复杂度O(nlogn),不稳定。

2.7归并排序

  算法思想:将两个(或者两个以上)的有序表合并成一个新的有序表,即把待排序序列分成若干个子序列,每个子序列都是有序的,然后再把有序子序列合并为整体有序序列。
  算法在排序的过程中会将整个序列分成两个数为一单元的组合,然后设定两个指针,最初位置分别为两个已经排序序列(一开始两个单独的数即为两个已经排序的序列)的起始位置,然后比较两个指针所指向的元素,选择相对小的元素放入到临时空间,并移动指针到下一位置,直到某一指针移动到序列尾,将另一序列剩下的所有元素直接复制到合并序列的尾部。
  归并排序示意图
  算法示例

//归并排序,时间复杂度O(nlogn),稳定
public class MergeSort {

    public static void main(String[] args) {
        int[] nums = {5,3,2,7,1,4,6};
        int low = 0;
        int high = nums.length - 1;
        startSort(nums,low,high);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

    // 归并排序
    public static int[] startSort(int[] nums, int low, int high) {
        // 找出中间的索引
        int mid = (low + high) / 2;
        if (low < high) {
            // 对左边数组进行递归
            startSort(nums, low, mid);
            // 对右边数组进行递归
            startSort(nums, mid + 1, high);
            // 左右归并
            merge(nums, low, mid, high);
        }
        return nums;
    }

    // 将左右两个有序的子序列进行合并
    public static void merge(int[] nums, int low, int mid, int high) {
        int[] temp = new int[high - low + 1];
        int i = low;// 左指针
        int j = mid + 1;// 右指针
        int k = 0;
        // 把较小的数先移动到新数组中
        while (i <= mid && j <= high) {
            if (nums[i] < nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }
        // 把左边剩余的数移入数组
        while (i <= mid) {
            temp[k++] = nums[i++];
        }
        // 把右边剩余的数移入数组
        while (j <= high) {
            temp[k++] = nums[j++];
        }
        // 把新数组中的数覆盖nums数组
        for (int k2 = 0; k2 < temp.length; k2++) {
            nums[k2 + low] = temp[k2];
        }
    }

}

  算法总结:算法复杂度O(nlogn),稳定。

2.8基数排序

  算法思想:将所有待比较值(正整数)统一为同样的数位长度,数位较短的数前面补0,然后从最低位开始,进行依次排序。这样从最低位一直到最高位排序完成以后,数列就变成了一个有序的序列。
  在众多的排序方法中,基数排序比较特殊,它是一种不需要进行关键字之间比较的排序方法,利用多关键字的划分,逐渐实现有序。
  比如:278,109,63,930,589,184,505,269,8,83进行排序的过程如下:
  (a)第一次分组,每一个元素每一位上的数值都是0~9,所以划分为10组,按照每个元素个位上的数值进行分组:
              0组:930
              1组:
              2组:
              3组:63,83
              4组:184
              5组:505
              6组:
              7组:
              8组:278,8
              9组:109,589,269
第一次排序后的结果:930,63,83,184,505,278,8,109,589,269
  (b)第二次分组,将第一次分组后的结果按照十位上的数进行分组:
              0组:505,8,109
              1组:
              2组:
              3组:930
              4组:
              5组:
              6组:63,269
              7组:278
              8组:83,184,589
              9组:
第二次排序后的结果:505,8,109,930,63,269,278,83,184,589
  (c)第三次分组,将第二次分组后的结果按照百位上的数进行分组:
              0组:8,63,83
              1组:109,184
              2组:278,269
              3组:
              4组:
              5组:505,589
              6组:
              7组:
              8组:
              9组:930
第三次排序后的结果:8,63,83,109,184,278,269,505,589,930
  (d)最高只有百位,没有千位,排序结束,输出结果。
  算法示例

import java.util.ArrayList;
import java.util.List;

//基数排序,时间复杂度O(nlogrm),稳定
public class RadixSort {

    public static void main(String[] args) {
        int[] nums = {5,3,2,7,1,4,6};
        nums = startSort(nums);
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println(nums[i]);
        }
    }

    // 基数排序
    public static int[] startSort(int[] nums) {
        int max = nums[0];
        // 选出数列中的最大值
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
        }
        // 计算总共需要比较多少次
        // 注意此处小于10的正整数除以10结果为0
        int time = 0;
        while (max > 0) {
            max /= 10;
            time++;
        }
        // 设定10个list保存数据
        List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
        for (int j = 0; j < 10; j++) {
            ArrayList<Integer> dataQueue = new ArrayList<Integer>();
            queue.add(dataQueue);
        }
        // 开始排序
        for (int i = 0; i < time; i++) {
            for (int j = 0; j < nums.length; j++) {
                // 取到当前这个数的对应位置上的一位数
                int x = nums[j] % (int) Math.pow(10, i + 1)
                        / (int) Math.pow(10, i);
                ArrayList<Integer> returnQueue = queue.get(x);
                returnQueue.add(nums[j]);
                queue.set(x, returnQueue);
            }
            int count = 0;
            for (int k = 0; k < 10; k++) {
                while (queue.get(k).size() > 0) {
                    ArrayList<Integer> tempQueue = queue.get(k);
                    nums[count] = tempQueue.get(0);
                    tempQueue.remove(0);
                    count++;
                }
            }
        }
        return nums;
    }

}

  算法总结:时间复杂度O(nlogrm),稳定。

3.各种排序算法的比较

排序算法平均时间复杂度最差时间复杂度稳定性空间复杂度备注
插入O(n^2)O(n^2)稳定O(1)基本有序
希尔O(nlogn)O(ns)不稳定O(1)s是所选分组
选择O(n^2)O(n^2)不稳定O(1)n小的时候
O(nlogn)O(nlogn)不稳定O(1)n大的时候
冒泡O(n^2)O(n^2)稳定O(1)n小的时候
快速O(nlogn)O(n^2)不稳定O(nlogn)n大的时候
归并O(nlogn)O(nlogn)稳定O(1)n大的时候
基数O(logRB)O(logRB)稳定O(n)B是对数(0-9),R是基数(个十百)

  总结:没有哪一种排序算法是绝对最优的,要看具体的应用场景。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值