排序算法总结

持续更新中。。。。。。

0、前言

根据比较与否,可将排序算法大致分为比较排序和非比较排序两大类:

  • 比较排序时间复杂度为O(nlogn) ~ O(n^2),主要包括:冒泡排序、选择排序、插入排序、归并排序、堆排序、快速排序等。
  • 非比较排序时间复杂度可达到线性级别,为O(n),主要包括:计数排序、基数排序、桶排序等。

主流排序算法性能比较:
排序算法总结_第1张图片

稳定性问题定义:
如果arr[i] = arr[j],排序前arr[i]在arr[j]之前,排序后arr[i]还在arr[j]之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。
对于以上比较类的排序算法而言,稳定性问题是可以在定义交换函数时打破这种稳定性的。

稳定性排序算法包括:

  • 冒泡排序
  • 直接插入排序
  • 归并排序
  • 基数排序
  • 计数排序
  • 桶排序

比较类的排序算法中,就复杂度最优情况而言:

  • 时间复杂度上:堆排序、归并排序稳定在O(nlogn)级别
  • 空间复杂度上:除了归并排序和快速排序,其它排序算法稳定在O(1)级别
  • 综合考虑来讲,稳定情况下归并排序的时间复杂度最优,只是空间复杂度为O(n)级别;不稳定和最坏的情况下,所有排序算法中,无论是时间还是空间上,堆排序都是最好的选择。但是堆排序比较和交换次数比快速排序多,所以平均而言比快速排序慢,也就是常数因子比快速排序大,如果你需要的是“排序”,那么绝大多数场合都应该用快速排序而不是其它的O(nlogn)算法。。

1、冒泡排序

原理很简单,类似于水中的一个气泡,从水底往上冒。冒泡排序就是需要进行n-1轮循环比较,每轮比较从0~n-1-i(i为次数编号)检查序列中的数,两两相邻进行比较,以升序排序为例就是:大的数往后放,这样进行完一次排序,大的数都是放在最后,直到所有次数排完也就是顺序的了。
最坏情况下需比较的次数:n(n-1)/2。
使用场景:n较小的情况。
Java代码实现:

   public static void bubbleSort2(int[] arr){
        for(int end = arr.length-1; end > 0; end--){
            int border = 0;
            for(int i = 0; i < end; i++){
                if(arr[i] > arr[i+1]){
                    swap(arr, i, i+1);  #交换函数,此处省略
                    border = i+1; #优化:记录此次结束位置,下次从该处进行比较
                }
            }
            end = border;
        }
    }

Python代码实现:

def bubbleSort(arr):
    n = len(arr)
    #进行n-1躺排序
    for i in range(1,n):
        is_ordered = True  # 是否有序序列标志
        #比较次数
        for j in range(n-i):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
                is_ordered = False
    if is_ordered:
        return arr

2、改进的冒泡排序(鸡尾酒排序)

在基本冒泡排序的基础上改进,采用两边同时冒泡的方法,也就是从左到右每次比较将大的数往后移的同时,从右到左将每次比较的最小数往前移,效率高于基本冒泡排序法。
Java代码实现:

 /**把最大的数往后面冒泡的同时,最小的数也往前面冒泡**/
    public static void cocktailSort(int[] arr) {
        int L = 0,R = arr.length-1;
        while(L < R) {
            for(int i = L; i < R; i++) if(arr[i] > arr[i+1]) swap(arr,i,i+1);
            R--;
            for(int i = R; i > L; i--) if(arr[i] < arr[i-1]) swap(arr,i,i-1);
            L++;
        }
    }

Python代码实现:

#鸡尾酒冒泡排序(改进版冒泡排序)
def cocktaiSort(arr):
    left = 0
    right = len(arr) - 1
    while left < right:
        #大的数往后排
        for i in range(left, right):
            if arr[i] > arr[i+1]:
                arr[i], arr[i+1] = arr[i+1], arr[i]
        right -= 1
        for j in range(right, left, -1):
            if arr[j] < arr[j-1]:
                arr[j], arr[j-1] = arr[j-1], arr[j]
        left += 1

    return arr

3、选择排序

选择排序顾名思义重在选择,那应该如何选择,每次比较选择怎样的数呢?
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

不稳定性主要表现在:
比如序列5 8 5 2 9,第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

最优时间复杂度:
即使已经是有序,还是得拿着前面的元素和后面的一个一个地进行比较,所以复杂度是O(n2)

最坏时间复杂度:
内层循环是和n有关的,复杂度是O(n2)

适用情况:n较小

图解:
排序算法总结_第2张图片
Java代码实现:

    public static void selectSort(int[] arr){
    	len = arr.length
        for(int i = 0; i < len - 1 ; i++) {
            //记录最小值的下标 
            int minIndex = i;
            for (int j = i + 1; j < len; j++)
            	 //从i+1开始遍历寻找最小值的索引
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            swap(arr,i,minIndex); //交换函数
        }
    }

Python代码实现:

def select_sort(arr):
    length = len(arr)
    for i in range(length-1):
        #记录最小值下标
        min_index = i #初始化为第一个数
        #从索引i+1后的序列开始遍历寻找最小值
        for j in range(i+1, length):
            if arr[min_index] > arr[j]:
                min_index = j
        #若已是最小值,就不交换
        if min_index != i:
            arr[i], arr[min_index] = arr[min_index], arr[i]
    return arr

4、插入排序

插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

可优化思路:
因为前一部分已经是从小到大排列的了, 所以如果从后面选出的最小元素大于前面的元素,那么就一定比前面的前面还要大,这时候就不需要进行比较了。

最优算法复杂度:
假设列表已经是从小到大排序好, 那么while循环进入一次就退出,总共进入n-1次,所以算法复杂度是O(n)。

最坏算法复杂度:
假设列表是完全无序,或者说是从大到小排列的,那么内层循环是n-1 n-2 n-3 …,和n是有关的,所以复杂度是O(n2)。

稳定性:
拿无序的第一个元素和前面的比较,比如说前面最大66,后面有一个66,因为后面的66不比前面的66大,位置不改变,所以插入排序是稳定的。

使用场景:数据量小时使用。并且大部分已经被排序。

Java代码实现:

   public static void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int key = arr[i], j;
            for (j = i - 1; j >= 0 && key < arr[j]; j--) arr[j + 1] = arr[j]; //中间的元素往后面移动
            arr[j + 1] = key;   //将key插入到合适的位置
        }
    }

Python代码实现:

def insert_sort(arr):
    length = len(arr)
    for i in range(1, length): #默认第一个数已排序,从索引1开始记作未排序
        for j in range(i, 0, -1): #从后往前遍历已排序数据并比较
            if arr[j] < arr[j-1]:
                arr[j], arr[j-1] = arr[j-1], arr[j] #交换位置即插入
            else:  #优化,如果未排序的第一个数大于等于已排序的最后一个数,则无需比较交换
                break
        print(arr)
    return arr

if __name__ == '__main__':
    arr = list(map(int, input().split(" ")))
    print(insert_sort(arr))

5、二分插入排序

二分插入排序是采用二分搜索法对插入排序进行改进。改进思路:在前面已经排好序的序列中找当前要插入的元素的位置时,采用二分查找的方式去找那个插入的位置(也就是大于key的那个位置) ,找到那个位置之后,再进行元素的移动,最后把那个元素插入到找到的那个位置。
Java代码实现:

public static void insertSort(int[] arr){
    int n = arr.length; 
    for(int i=1; i<n; i++){
        int key = arr[i];
        //二分查找已排序序列
        int left = 0;
        int right = i -1;
        while(left <= right){
            int mid = left + (right - left)/2;
            if(arr[mid] > key){  //说明在左半部分,则缩小右边界
                right = mid -1;
            }else{  //否则在右半部分,缩小左边界
                left = mid + 1;
            }
        }
        //二分查找完后,只需要从左边界遍历即可,也就是刚好大于key的那个位置
        for(int j=i-1; j>=left; j--) arr[j+1] = arr[j];
        arr[j+1] = key;
    }
}

Python代码:

def insert_sort_impl(arr):
    n = len(arr)
    for i in range(1, n):
        key = arr[i]
        l, r = 0, i-1
        while l <= r:
            mid = l + (r - l) // 2 # mid = (l + r) // 2
            if arr[mid] > key:
                r = mid -1
            else:
                l = mid + 1
        #只需移动l即之后的位置
        for j in range(i-1, l-1, -1):
            arr[j+1] = arr[j]
        arr[l] = key #原左边界的位置就是要插入的元素的位置
    return arr

if __name__ == '__main__':
    arr = list(map(int, input().split(" ")))
    print(insert_sort_impl(arr))

6、希尔排序

希尔排序是插入排序的进阶版,算法思路:
(1)按步长把原来的序列分为好几部分,每一个部分采用插入排序;
(2)调整步长,重复这个过程
希尔排序相对于插入排序的优势在于插入排序每次只能将数据移动一位,不过希尔排序时间复杂度的大小还是要取决于步长的合适度,另外希尔排序不是一种稳定的排序算法。
图解:
排序算法总结_第3张图片
Java代码实现:

 public static void shellSort2(int[] arr) {
     for(int gap = arr.length; gap > 0; gap /= 2) {     //增量序列
         for(int i = gap; i < arr.length; i++) {        //从数组第gap个元素开始
             int key = arr[i],j;                        //每个元素与自己组内的数据进行直接插入排序
             for(j = i-gap; j >= 0 && key < arr[j]; j -= gap) arr[j+gap] = arr[j];
             arr[j+gap] = key;
         }
     }
 }

Python代码实现:

def shell_sort(arr):
    length = len(arr)
    gap = length // 2
    while gap > 0: #步长必须能取到1
        for i in range(gap,length): #插入排序写法,不同的地方是加入了步长
            for j in range(i, 0, -gap):
                if arr[j] < arr[j - gap]:
                    arr[j], arr[j-gap] = arr[j-gap], arr[j]
                else:
                    break
        gap //= 2
    return arr

if __name__ == '__main__':
    arr = list(map(int, input().split(" ")))
    print(shell_sort(arr))

7、快速排序

快速排序的思想其实很简单:首先找一个基准数,一般方便直接以数组第一个数作为基准数,然后初始化两个最左和最右指针,先移动最右的指针,找到比基准数小的数就停止,然后移动最左边的指针找到比基准数大的数停止,此时交换两个指针指向的数,然后继续右指针先移动重复该过程,知道两个指针相遇,此时交换基准数和两个指针共同指向的那个数,这样就完成了第一轮的划为,以基准数为划分点,做左边的都是小于基准数的,最右边的都是大于基准数的。后续再用递归快排下左右两个部分就好了。
图解可以参考一下这篇博客:https://blog.csdn.net/shujuelin/article/details/82423852
使用场景:是最快的通用排序算法,大多数使用情况下,是最佳选择。
Java代码实现:

public class QuickSort {
    public static void quickSort(int[] arr, int low, int height){
        int i, j, temp, t;
        if(low > height){
            return;
        }
        i = low; //左指针
        j = height;  //右指针
        temp = arr[i]; //基准数
        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;
            }
        }
        //交换基准数与两指针相遇时指向的数
        arr[low] = arr[i];
        arr[i] = temp;
        //递归
        quickSort(arr, low, j-1);
        quickSort(arr, j+1, height);
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        String[] strs = str.split(" ");
        int[] arr = new int[strs.length];
        for(int i=0; i<strs.length; i++){
            arr[i] = Integer.parseInt(strs[i]);
        }
        quickSort(arr, 0, arr.length-1);
        System.out.println(Arrays.toString(arr));
    }
}

8、归并排序

归并排序思路:将一个待排序的数组不断分成左右两部分进行递归排序,最后合并已排好序的即可。
归并排序也是分治法一个很好的应用,先递归到最底层,然后从下往上每次两个序列进行归并合起来,是一个由上往下分开,再由下往上合并的过程,而对于每一次合并操作,过程如下:
(1)申请一个额外的辅助数组空间,长度与原数组长度一样,用来存放合并好的序列;
(2)设置两个指针,起始位置分别为左右排好序的起始位置;
(3)比较两个指针指向的数,并将较小的数存放到合并数组中,然后继续移动指针,指针指向末尾结束;
(4)将剩余的数直接复制到合并数组的末尾。
使用场景:如果需要稳定,空间不是很重要,就选择归并排序。
Java代码实现:

public class MergeSort {

   public static void mergeSort(int[] arr) {
      if (arr == null || arr.length < 2) {
         return;
      }
      mergeSort(arr, 0, arr.length - 1);
   }

   public static void mergeSort(int[] arr, int l, int r) {
      if (l == r) {
         return;
      }
      int mid = l + ((r - l) >> 1); //mid = (L+R)/2,取中点,但这种写法容易溢出
      //所以文中写法相当于mid=L+(R-L)/2,然后位运算效率更快
      mergeSort(arr, l, mid);//T(n/2)
      mergeSort(arr, mid + 1, r);//T(n/2)
      merge(arr, l, mid, r);//O(N)
      //T(N)=2T(N/2)+O(N)
   }
   //合并
   public static void merge(int[] arr, int l, int m, int r) {
      int[] help = new int[r - l + 1];
      int i = 0;
      int p1 = l;
      int p2 = m + 1;
      while (p1 <= m && p2 <= r) {
         help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
      }
      
      //两个必有一个越界
      while (p1 <= m) {
         help[i++] = arr[p1++];
      }
      while (p2 <= r) {
         help[i++] = arr[p2++];
      }
      for (i = 0; i < help.length; i++) {
         arr[l + i] = help[i];
      }
   }
}

9、堆排序

(1)堆的基本性质:

  • 堆首先是一颗完全二叉树;
  • 堆要满足的性质是:或者每一个结点都大于孩子,或者都小于孩子结点的值;
  • 最大堆: 所有结点都比自己的孩子结点大;
  • 最小堆: 所有结点都比自己的孩子结点小;
    例如:
    排序算法总结_第4张图片
    同时,对堆中的结点按层进行编号,将这种逻辑结构映射到数组中:
    排序算法总结_第5张图片
    该数组从逻辑上讲就是一个堆结构,用简单的公式来描述一下堆的定义:
    大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
    小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
    (2)堆排序:
    堆排序的基本思想是:
    将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。简单的描述下步骤:
  • 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  • 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  • 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
    堆排序稳定性:
    堆排序是不稳定的算法,它不满足稳定算法的定义。它在交换数据的时候,是比较父结点和子节点之间的数据,所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化。

堆排序的适用场景有两个:

  • 比如最大/小的元素,topK之类。用堆排序可以在N个元素中找到top K,时间复杂度是O(N log
    K),空间复杂的是O(K),而快速排序的空间复杂度是O(N),也就是说,如果你要在很多元素中找很少几个top
    K的元素,或者在一个巨大的数据流里找到top K,快速排序是不合适的,堆排序更省地方。
  • 优先队列,需要在一组不停更新的数据中不停地找最大/小元素,快速排序也不合适。

(3)Java代码实现:

public class HeapSort {
    public static void sort(int[] arr){
        //1、构建初始大顶堆,从第一非叶子节点开始
        for(int i=arr.length/2 - 1; i>=0; i--){
            adjustHeap(arr, i, arr.length);
        }

        //2、调整继续调整堆结构并交换堆顶元素
        for(int j=arr.length-1; j>0; j--){
            //交换函数
            swap(arr, 0, j);
            //重新构建堆,重复步骤1
            adjustHeap(arr, 0, j);
        }
    }

    private static void adjustHeap(int[] arr, int i, int length){
        //保存当前结点
        int temp = arr[i];
        //从i结点的左子节点开始,也就是2*i+1
        for(int k=i*2+1; k<length; k=k*2+1){
            //如果左子节点小于右子节点,k指向右子节点
            if(k+1<length && arr[k]<arr[k+1]){
                k++;
            }
            //如果子节点大于父节点,则和父节点进行交换
            if(arr[k] > temp){
                arr[i] = arr[k]; //将大的结点作为父节点
                i = k;
            }else{
                break;
            }
        }
        arr[i] = temp; // 将temp值放到尾部
    }

    private static void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        //获取输入的字符串序列
        String str = sc.nextLine(); //输入一行字符串
        String[] strs = str.split(" "); //空格进行切割并保存为字符串数组

        //转换为整数数组
        int[] arr = new int[strs.length];
        for(int i=0; i<strs.length; i++){
            arr[i] = Integer.parseInt(strs[i]);
        }
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

总结:
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。

10、计数排序

待更新。。。。。。

11、基数排序

待更新。。。。。。

12、桶排序

待更新。。。。。。

参考资料:
[1]https://blog.csdn.net/zxzxzx0119/article/details/79826380#t13
[2]https://www.cnblogs.com/ladder/p/10685051.html
[3]https://blog.csdn.net/u014736619/article/details/80549698
[4]https://blog.csdn.net/shujuelin/article/details/82423852

你可能感兴趣的:(数据结构与算法)