zoukankan      html  css  js  c++  java
  • 常用排序算法总结

    排序算法

    1. 排序算法概述

    1.1 什么是排序算法?

    对一序列对象根据某个关键字,按照某种规则进行排序

    1.2 排序术语

    • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面
    • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面
    • 内排序:所有排序操作都在内存中完成
    • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
    • 时间复杂度: 一个算法执行所耗费的时间。
      空间复杂度:运行完一个程序所需内存的大小

    1.3 算法总结

    排序算法比较

    • (注意:n指数据规模;k指“桶”的个数;In-place指占用常数内存,不占用额外内存;Out-place指占用额外内存)

    1.4 算法分类

    clipboard

    1.5 比较排序与非比较排序

    1.5.1 比较排序

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

    1.5.2 非比较排序

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

    2. 排序算法具体实现

    2.1 冒泡排序

    2.1.1 算法思想

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

    2.1.2 算法描述

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

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

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

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

      从前往后将最大的拍到后面

      冒泡排序

      从后往前将最小的排到前面

      冒泡排序02

    2.1.3 算法实现

    (1)数组线性表
    /**
     * 冒泡排序-数组线性表排序算法
     */
    public void bubbleSort(){
        if(this.size <= 1){
            return ;
        }
        for(int i = 0;i<this.size;i++){
            boolean didSwap = false;
            for(int j = 0;j<this.size-i-1;j++){
                if(((Comparable<E>)objects[j+1]).compareTo((E)objects[j])<0){
                    Comparable<E> temp =(Comparable<E>) objects[j+1];
                    objects[j+1] = objects[j];
                    objects[j] = temp;
                    didSwap = true;
                }
            }
            if(!didSwap){
                return ;
            }
        }
    }
    
    (2)链式线性表
    /**
     * 冒泡排序-链式表排序算法
     */
    public void bubbleSort(){
        if(this.head.next == null||this.head.next.next==null){
            return ;
        }
        //后面已经浮上来的(排好序的)头结点
        Node<E> sortedHead = null;
        //第一个存储元素的结点
        Node<E> first = head.next;
        for(Node<E> outCur = head.next;outCur!=null;outCur = outCur.next){
            Node<E> cur = first;
            for(Node<E> afterCur = first.next;afterCur!=sortedHead;afterCur = afterCur.next,cur = cur.next){
                if(cur.elem.compareTo(afterCur.elem)>0){
                    Node.swapElem(cur, afterCur);
                }
            }
            sortedHead = cur;
        }
    }
    

    2.1.4 算法分析

    • 最佳情况:(T(n) = O(n))
      • 此时数组为正序排列,如果第一次检测没有发生交换,则可以认为为正序排列,则可以直接退出
    • 最差情况:(T(n) = O(n^2))
      • 逆序排列,对于每个位置都需要进行交换
    • 平均情况:(T(n) = O(n^2))

    2.2排序

    2.2.1 算法思想

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

    2.2.2 算法描述

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

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

    2. 第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个的新无序区;

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

    选择排序

    2.2.3 算法实现

    (1)数组线性表

    public class SelectionSorter<E extends Comparable<E>> implements Sorter<E> {
        @Override
        public void sort(E[] elemArr){
            for(int i = 0;i < elemArr.length;i++){
                int minIndex = i;
                for(int j = i;j<elemArr.length;j++){
                    if(elemArr[j].compareTo(elemArr[minIndex])<0){
                        minIndex = j;
                    }
                }
                E temp = elemArr[i];
                elemArr[i] = elemArr[minIndex];
                elemArr[minIndex] = temp;
            }
        }
    }
    

    (2)链式线性表

    /**
     * 插入排序
     */
    public void selectionSort(){
        if(this.head.next==null||this.head.next.next==null){
            return ;
        }
    
        for(Node<E> outCur = head.next;outCur!=null;outCur = outCur.next){
    
            Node<E> innerCur = outCur;
            Node<E> afterInnerCur = innerCur.next;
            Node<E> minNode = outCur;
            for(innerCur = outCur;afterInnerCur!=null;innerCur = innerCur.next,afterInnerCur = afterInnerCur.next){
                if(innerCur.elem.compareTo(minNode.elem)<0){
                    minNode = innerCur;
                }
            }
            Node.swapElem(outCur,minNode);
        }
    }
    

    2.3 插入排序

    2.3.1 算法思想

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

    2.3.2 算法描述

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

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

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

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

    5. 将新元素插入到该位置后

    6. 重复步骤2~5

      插入排序

      插入排序02

    2.3.3 算法实现

    public class InsertSorter<E extends Comparable<E>> implements Sorter<E> {
        @Override
        public void sort(E[] arr) {
            for (int i = 1; i < arr.length; i++) {
                for (int j = i;j > 0;--j) {
                    if (arr[j].compareTo(arr[j - 1]) < 0) {
                        E temp = arr[j];
                        arr[j] = arr[j - 1];
                        arr[j - 1] = temp;
                    } else {
                        break;
                    }
                }
            }
        }
    }
    

    2.3.4 算法分析

    • 最佳情况:(T(n) = O(n)) 序列本身有序(每次选取的第一个无序点都大于有序序列的最大值)
    • 最差情况:(T(n) = O(n^2)) 序列为逆序(每次选取的无序序列都需要移动到有序序列最前面(1+2+...+(n-1)=frac{(n-1)n}{2})
    • 平均情况:(T(n) = O(n^2))

    2.4 希尔排序

    2.4.1 算法思想

    • 希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破(O(n^2))的第一批算法之一。
    • 它与插入排序的不同之处在于,它会优先比较距离较远的元素。
    • 希尔排序又叫缩小增量排序。希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
    • 该方法实质上是一种分组插入方法
    • 增量序列的最后一个增量值必须等于1

    2.4.2 算法描述

    1. 选择增量(gap=length/2),缩小增量继续以(gap = gap/2)的方式,这种增量选择我们可以用一个序列来表示,(T={n/2,(n/2)/2,...,1}),称为增量序列。
      • 采用上述增量序列因为增量序列难以通过数学证明求出,所以选择一个常用的增量序列 ,上述增量序列也被称为希尔增量。
        • 但其实这个增量序列不是最优的
    2. 按增量序列个数k,对序列进行k 趟排序
    3. 每趟排序,根据对应的增量ti,将待排序列分成若干长度为m 的子序列,分别对各子表进行直接插入排序。
    4. 仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度
      img
    5. img

    2.4.3 算法实现

    package com.sort;
    
    
    /**
     * @author Ni187
     */
    public class ShellSorter<E extends Comparable<E>> implements Sorter<E> {
        @Override
        public void sort(E[] arr) {
            for (int gap = arr.length / 2; gap > 0; gap /= 2){
                for(int i=gap; i<arr.length; i++){
                    for(int j=i-gap; j>=0&&arr[j].compareTo(arr[j+gap])>0; j=j-gap){
                        E temp = arr[j];
                        arr[j] = arr[j+gap];
                        arr[j+gap] = temp;
                    }
                }
            }
        }
    }
    
    
    

    2.4.4 算法分析

    • 最佳情况:(T(n)=O(n^2))
    • 最坏情况:(T(n)=O(nlog(2n)))
    • 平均情况:(T(n)=O(nlog(2n)))

    2.5 归并排序

    2.5.1 算法思想

    • 归并排序是建立在归并操作上的一种有效的排序算法。
    • 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
    • 归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并

    2.5.2 算法描述

    1. 把长度为n的输入序列分成两个长度为n/2的子序列

    2. 对这两个子序列分别采用归并排序

    3. 将两个排序好的子序列合并成一个最终的排序序列
      归并排序02

      归并排序

    2.5.3 算法实现

    package com.sort;
    
    import java.util.Arrays;
    
    /**
     * @author Ni187
     */
    public class MergeSorter<E extends Comparable<E>> implements Sorter<E> {
    
        @Override
        public void sort(E[] arr) {
            Object[] mergedArr = new Object[arr.length];
            sort(arr, 0, arr.length-1,mergedArr);
        }
    
        public void sort(E[] arr, int left, int right,Object[] mergedArr) {
            if (right<=left) {
                return;
            }
            int mid = (left + right) / 2;
            sort(arr, left, mid,mergedArr);
            sort(arr, mid+1 , right,mergedArr);
            merge(arr, left, mid, right,mergedArr);
        }
    
        private void merge(E[] arr, int left, int mid, int right,Object[] mergedArr) {
            int i = left;
            int j = mid + 1;
            int index = 0;
            while (i <= mid && j <= right) {
                if (arr[i].compareTo(arr[j]) <= 0) {
                    mergedArr[index++] = arr[i++];
                } else {
                    mergedArr[index++] = arr[j++];
                }
            }
            while (i <= mid) {
                mergedArr[index++] = arr[i++];
            }
            while (j <= right) {
                mergedArr[index++] = arr[j++];
            }
            index = 0;
            while(left <= right){
                arr[left++] = (E)mergedArr[index++];
            }
        }
    
    }
    
    

    2.5.5 算法分析

    • 每次合并操作的平均时间复杂度为(O(n)),而完全二叉树的深度为(logn)。总的平均时间复杂度为(O(nlogn))
    • 归并排序的最好,最坏,平均时间复杂度均为(O(nlogn))
    • 由于使用辅助数组来存储归并的数组,空间复杂度为(O(n))

    2.6 快速排序

    2.6.1 算法思想

    • 通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序

    2.6.2 算法描述

    1. 从数列中挑出一个元素,称为 “基准”(pivot);
    2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
      • 挖坑法
      • 交换法
    3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
      快速排序

    2.6.3 算法实现

    • 此处使用交换法实现
    package com.sort;
    
    import java.util.Random;
    
    /**
     * @author Ni187
     */
    public class QuickSorter<E extends Comparable<E>> implements Sorter<E> {
        @Override
        public void sort(E[] arr) {
            QuickSort(arr, 0, arr.length-1);
        }
    
        public  void QuickSort(E[] array, int start, int end) {
            if (start > end) {
                return ;
            }
            int smallIndex = partition(array, start, end);
            QuickSort(array, start, smallIndex - 1);
            QuickSort(array, smallIndex + 1, end);
        }
    
        public int partition(E[] array, int left, int right) {
            int pivot = (int) (left + Math.random() * (right - left));
            E basic = array[pivot];
            while(left!=right){
                while(left < right && array[right].compareTo(basic)>=0){
                    right--;
                }
                while(left < right && array[left].compareTo(basic)<=0){
                    left++;
                }
                swap(array, left, right);
            }
            array[left]=basic;
            return left;
        }
    
        public void swap(E[] array, int i, int j) {
            E temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }
    

    2.5.4 算法分析

    • 最好情况:每次选择的基数恰好把两侧分开(左侧小于基数,右侧大于基数),每次扫描n,共分成logn,时间复杂度为(O(logn))
    • 最坏情况:每次选择的基数恰好位于两侧之一,每次扫描n,共分成n,时间复杂度为(O(n^2))
    • 平均:复杂度(O(nlog n))
  • 相关阅读:
    .NET设计模式(19):观察者模式(Observer Pattern)
    漂亮的信息提示
    工厂方法模式(Factory Method)
    .NET设计模式(16):模版方法(Template Method)
    通过序列化实现深拷贝
    C#委托的妙文,让你知道如何发挥委托的作用
    .NET设计模式(12):外观模式(Façade Pattern)
    动态方法DynamicMethod简例
    ado.net中的自动获取存储过程参数
    设计模式之——简单工厂模式
  • 原文地址:https://www.cnblogs.com/nishoushun/p/12600480.html
Copyright © 2011-2022 走看看