十大经典排序算法(待更新)

前言

  所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是i如何使得记录按照要求排序的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方法,一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制喝规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析

介绍

  排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法:插入、希尔、选择、冒泡、归并、快速、堆、基数排序等。本文也只讲解内部排序算法。用一张图概括:

   PS:
  名称解释:n:数据规模,k:“桶”的个数,In-place:占用常数内存,不占用额外内存,Out-place:占用额外内存

  术语解释:稳定:如果A原本在B前面,而A=B,排序之后A仍然在B的前面。

       不稳定:如果A原本在B前面,而A=B,排序之后可能出现在B的后面。

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

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

       时间复杂度:定性描述一个算法执行所耗费的时间。

       空间复杂度:定性描述一个算法执行所需要内存的大小。

算法分类

  十种常见排序算法可以分类两大类别:比较类排序和非比较类排序。

 

   常见的快速排序、归并排序、推排序以及冒泡排序等都属于比较类排序算法。比较类排序是通过比较来决定元素的相对次序,由于其时间复杂度不能突破O(nlogn),因此也成为非线性时间比较类排序。在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n^2)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logn次,所以平均时间复杂度为O(nlogn)。

  比较类排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说比较排序适用于一切需要排序的情况。

  而计数排序、基数排序、桶排序则属于非比较类排序算法。非比较类排序不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序,由于它可以突破比较类排序的时间下界,以线性时间运行,因此称为线性非比较类排序。非比较排序只要确定每个元素前的已有元素个数即可,所以一次遍历即可解决。

冒泡排序

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

算法步骤

  1.比较相邻地元素,如果第一个比第二个大,就交换他们两个;2.对每一对相邻元素作同样地工作,从开始第一对到结尾地最后一对,这样在最后地元素应该会是最大地数;3.针对所有元素重复以上步骤,除了最后一个;4.重复步骤1~3,直到排序完成。

图解算法

代码实现

    /**
     * 冒泡排序
     * 时间复杂度 O(n^2)
     * 空间复杂度 O(1)
     * @param arr
     */
    public static int[] BubbleSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            boolean flag = true;
            for (int j = 0; j < arr.length - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    int tmp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = tmp;
                    flag = false;
                }
            }
            // 经过一轮比较没有元素移动,说明已经有序了,可以退出
            if (flag) {
                break;
            }
        }
        return arr;
    }

  此处对代码做了一个小优化,加入了flag标识,目的是将算法地最佳时间复杂度优化为O(n),即当原输入就是排序好的情况下,该算法的时间复杂度就是O(n)。

选择排序

  选择排序是一种简单直观的排序算法,无论什么数据进去都是O(n^2)的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处就是不占额外的内存空间。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列末尾,以此类推,直到所有元素排序完毕。

算法步骤

  1.首先在未排序序列中找到最小(大)元素,存放到排序序列的其实位置。

  2.再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

  3.重复第2步,直到所有元素排序完毕。

图解算法

代码实现

/**
     * 选择排序
     * 时间复杂度 O(n^2)
     * 空间复杂度 O(1)
     * @param arr
     * @return
     */
    public static int[] selectionSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[minIndex] > arr[j]) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                int tmp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = tmp;
            }
        }
        return arr;
    }

插入排序

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

  插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的,就如同平时打扑克牌一样,把未排序的牌,插入到已经排好的牌里。

  插入排序和冒泡排序一样,也有一种优化算法,叫做折半插入。

算法步骤

  1.从第一个元素开始,该元素可以认为已经被排序;2.取出下一个元素,在已经排序的元素序列中从后面向前扫描;3.如果扫描到的元素大于新元素,将扫描的的元素移到下一位置;4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;5.将新元素插入到该位置后;6.重复2~5。

图解算法

代码实现

    /**
     * 直接插入排序
     * 时间复杂度 O(n^2)
     * 空间复杂度 O(1)
     * @param arr
     * @return
     */
    public static int[] insertionSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int preIndex = i - 1;
            int current = arr[i];
            while (preIndex >= 0 && current < arr[preIndex]) {
                arr[preIndex + 1] = arr[preIndex];
                preIndex--;
            }
            arr[preIndex + 1] = current;
        }
        return arr;
    }

希尔排序

  希尔排序是希尔于1959年提出的一种排序算法,希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高的版本,也称为递减增量排序算法,同时该算法是冲破O(n^2)的第一批算法之一。

  希尔排序的基本思想是:先将整个待排序的记录分割成为若干子序列分别进行直接插入排序,具体算法描述:

  1.选择一个增量序列{t1,t2,t3,...,tk},其中 ti>tj, i<j;2.按增量序列个数k,对序列进行k趟排序;3.每趟排序,根据对应的增量t,将待排序分割成若干长度为m的子序列,分别对各子序列进行直接插入排序,仅增量因子为1时,整个序列作为一个来处理,长度即为整个序列长度。

图解算法

 

 代码实现

/**
 * 希尔排序
 *
 * @param arr
 * @return arr
 */
public static int[] ShellSort(int[] arr) {
    int n = arr.length;
    int gap = n / 2;
    while (gap > 0) {
        for (int i = gap; i < n; i++) {
            int current = arr[i];
            int preIndex = i - gap;
            // Insertion sort
            while (preIndex >= 0 && arr[preIndex] > current) {
                arr[preIndex + gap] = arr[preIndex];
                preIndex -= gap;
            }
            arr[preIndex + gap] = current;

        }
        gap /= 2;
    }
    return arr;
}

 归并排序

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

  和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价使需要额外的内存空间。

算法步骤

  归并排序算法是一个递归过程,边界条件(递归算法都会有一个边界条件)为当输入序列仅有一个元素时,直接返回,具体过程如下:

  1.如果输入只有一个元素,则直接返回,否则将长度为n的输入序列分成两个长度为n/2的子序列;

  2.分别对这两个子序列进行归并排序,使子序列变为有序状态;

  3.设定两个指针,分别指向两个已经排序子序列的起始位置;

  4.比较两个指针所指向的元素,选择较小的元素放入到合并空间(用于存放排序结果),并移动指针到下一个位置;

  5.重复步骤3-4直到某一指针达到序列尾;

  6.将另一序列剩下的所有元素直接复制到合并序列尾。

图解算法

  

代码实现

/**
 * 归并排序
 *
 * @param arr
 * @return arr
 */
public static int[] MergeSort(int[] arr) {
    if (arr.length <= 1) {
        return arr;
    }
    int middle = arr.length / 2;
    int[] arr_1 = Arrays.copyOfRange(arr, 0, middle);
    int[] arr_2 = Arrays.copyOfRange(arr, middle, arr.length);
    return Merge(MergeSort(arr_1), MergeSort(arr_2));
}

/**
 * Merge two sorted arrays
 * 
 * @param arr_1
 * @param arr_2
 * @return sorted_arr
 */
public static int[] Merge(int[] arr_1, int[] arr_2) {
    int[] sorted_arr = new int[arr_1.length + arr_2.length];
    int idx = 0, idx_1 = 0, idx_2 = 0;
    while (idx_1 < arr_1.length && idx_2 < arr_2.length) {
        if (arr_1[idx_1] < arr_2[idx_2]) {
            sorted_arr[idx] = arr_1[idx_1];
            idx_1 += 1;
        } else {
            sorted_arr[idx] = arr_2[idx_2];
            idx_2 += 1;
        }
        idx += 1;
    }
    if (idx_1 < arr_1.length) {
        while (idx_1 < arr_1.length) {
            sorted_arr[idx] = arr_1[idx_1];
            idx_1 += 1;
            idx += 1;
        }
    } else {
        while (idx_2 < arr_2.length) {
            sorted_arr[idx] = arr_2[idx_2];
            idx_2 += 1;
            idx += 1;
        }
    }
    return sorted_arr;
}

算法分析

  稳定性:稳地,时间复杂度:最佳:O(nlogn)、最差:O(nlogn)、平均:O(nlogn),空间复杂度:O(n)

快速排序

  快速排序用到了分治思想,同样的还有归并排序,咋看起来快速排序和归并排序非常相似,都是将问题变小,先排序子序列,最后合并。不同的是快排在划分子问题的时候经过多一步处理,将划分的两组数据划分为一大一小,这样在最后合并的时候就不必像归并那样再进行比较,但也因为如此,划分的不定性使得快排的时间复杂度不稳地。

  快排的基本思想:通过一趟排序将待排序列分割成独立的两部分,其中一部分记录元素均比另一部分的元素小,则可分别对这两部分子序列继续进行排序,以达到整个序列有序。

算法步骤

  快排使用分治法策略来把一个序列分为2个子序列,然后递归地排序两个子序列。具体算法描述如下:

  1.从序列中随机跳出一个元素,作为“基准”(pivot);

  2.重新排序序列,将所有比基准值小的元素摆放再基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任何一边)。在这个操作结束之后,该基准就处于数列中间文职。这个称为分区操作;

  3.递归地把小于基准值元素地子序列和大于基准值元素地子序列进行快速排序。

图解算法

代码实现 

/**
 * Swap the two elements of an array
 * @param array
 * @param i
 * @param j
 */
private static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

/**
 * Partition function
 * @param arr
 * @param left
 * @param right
 * @return small_idx
 */
private static int Partition(int[] arr, int left, int right) {
    if (left == right) {
        return left;
    }
    // random pivot
    int pivot = (int) (left + Math.random() * (right - left + 1));
    swap(arr, pivot, right);
    int small_idx = left;
    for (int i = small_idx; i < right; i++) {
        if (arr[i] < arr[right]) {
            swap(arr, i, small_idx);
            small_idx++;
        }
    }
    swap(arr, small_idx, right);
    return small_idx;
}

/**
 * Quick sort function
 * @param arr
 * @param left
 * @param right
 * @return arr
 */
public static int[] QuickSort(int[] arr, int left, int right) {
    if (left < right) {
        // 分区操作
        int pivotIndex = Partition(arr, left, right);
        QuickSort(arr, left, pivotIndex - 1);
        QuickSort(arr, pivotIndex + 1, right);
    }
    return arr;
}

推排序

  (待更新)

计数排序

  计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中,作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

  计数排序是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。他只能对整数进行排序。

算法步骤

  1.找出数组中最大值max、最小值min;

  2.创建一个新数组C,其长度是max-min+1,其元素默认值都为0;

  3.遍历原数组A中的元素A[i],以A[i] - min 作为C数组的索引,以A[i]的值在A中元素出现次数作为C[A[i] - min]的值;

  4.对C数组变形,新元素的值是该元素与前一个元素值的和,即当i>1时C[i] = C[i] + C[i-1];

  5.创建结果数组R,长度和原始数组一样;

  6.从后向前遍历原始数组A中的元素A[i],使用A[i]减去最小值min作为索引,在计数数组C中找到对应的值C[A[i] - min],C[A[i] - min] - 1就是A[i]在结果数组R中的位置,做完上述这些操作,将count[A[i]-min]减小1。

图解算法

代码实现

/**
 * Gets the maximum and minimum values in the array
 * 
 * @param arr
 * @return
 */
private static int[] getMinAndMax(int[] arr) {
    int maxValue = arr[0];
    int minValue = arr[0];
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] > maxValue) {
            maxValue = arr[i];
        } else if (arr[i] < minValue) {
            minValue = arr[i];
        }
    }
    return new int[] { minValue, maxValue };
}

/**
 * Counting Sort
 * 
 * @param arr
 * @return
 */
public static int[] CountingSort(int[] arr) {
    if (arr.length < 2) {
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int[] countArr = new int[maxValue - minValue + 1];
    int[] result = new int[arr.length];

    for (int i = 0; i < arr.length; i++) {
        countArr[arr[i] - minValue] += 1;
    }
    for (int i = 1; i < countArr.length; i++) {
        countArr[i] += countArr[i - 1];
    }
    for (int i = arr.length - 1; i >= 0; i--) {
        int idx = countArr[arr[i] - minValue] - 1;
        result[idx] = arr[i];
        countArr[arr[i] - minValue] -= 1;
    }
    return result;
}

算法分析

  当输入的元素是n个0到k之间的整数时,他的运行时间是O(n+k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量额外空间。

桶排序

  桶排序是计数排序的升级版。他利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,需要做到这两点:在额外空间充足的情况下,尽量增大桶的数量;使用的映射函数能够将输入的N个数据均匀的分配到K个桶中。

算法步骤

  1.设置一个BucketSize,作为每个桶所能放置多少个不同数值;

  2.遍历输入数据,并且把数据依次映射到对应的桶里去;

  3.对每个非空的桶排序,可以使用其他排序方法,也可以递归使用桶排序;

  4.从非空桶里把排好序的数据拼接起来。

图解算法

代码实现

/**
 * Gets the maximum and minimum values in the array
 * @param arr
 * @return
 */
private static int[] getMinAndMax(List<Integer> arr) {
    int maxValue = arr.get(0);
    int minValue = arr.get(0);
    for (int i : arr) {
        if (i > maxValue) {
            maxValue = i;
        } else if (i < minValue) {
            minValue = i;
        }
    }
    return new int[] { minValue, maxValue };
}

/**
 * Bucket Sort
 * @param arr
 * @return
 */
public static List<Integer> BucketSort(List<Integer> arr, int bucket_size) {
    if (arr.size() < 2 || bucket_size == 0) {
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int bucket_cnt = (maxValue - minValue) / bucket_size + 1;
    List<List<Integer>> buckets = new ArrayList<>();
    for (int i = 0; i < bucket_cnt; i++) {
        buckets.add(new ArrayList<Integer>());
    }
    for (int element : arr) {
        int idx = (element - minValue) / bucket_size;
        buckets.get(idx).add(element);
    }
    for (int i = 0; i < buckets.size(); i++) {
        if (buckets.get(i).size() > 1) {
            buckets.set(i, sort(buckets.get(i), bucket_size / 2));
        }
    }
    ArrayList<Integer> result = new ArrayList<>();
    for (List<Integer> bucket : buckets) {
        for (int element : bucket) {
            result.add(element);
        }
    }
    return result;
}

算法分析

   稳定性:稳地;时间复杂度:最佳O(n+k) 最差:O(n^2) 平均:O(n+k);空间复杂度O(k)。

基数排序

  基数排序也是非比较的排序算法,对元素中的每一位数字进行排序,从最低位开始排序,复杂度为O(n*k),n为数组长度,k为数组中元素的最大的位数;

  基数排序是按照低位先排序。然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性使又优先级排序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以稳定的。

算法步骤

  1.取得数组中的最大数,并取得位数,即为迭代次数N(例如:数组中最大数值为1000,则N=4);

  2.A为原始数组,从最低位开始取每个组成radix数组;

  3.对radix进行计数排序(利用计数排序适用于小范围数的特点);

  4.将radix依次赋值给原数组;

  5.重复2~4步骤N次;

图解算法

代码实现

/**
 * Radix Sort
 * 
 * @param arr
 * @return
 */
public static int[] RadixSort(int[] arr) {
    if (arr.length < 2) {
        return arr;
    }
    int N = 1;
    int maxValue = arr[0];
  // 获得最大值
for (int element : arr) { if (element > maxValue) { maxValue = element; } }
  // 获得最大位数
while (maxValue / 10 != 0) { maxValue = maxValue / 10; N += 1; } for (int i = 0; i < N; i++) { List<List<Integer>> radix = new ArrayList<>(); for (int k = 0; k < 10; k++) {
       // 初始化 radix.add(
new ArrayList<Integer>()); } for (int element : arr) { int idx = (element / (int) Math.pow(10, i)) % 10; radix.get(idx).add(element); } int idx = 0; for (List<Integer> l : radix) { for (int n : l) { arr[idx++] = n; } } } return arr; }

算法分析

  稳定性:稳地;时间复杂度:最佳O(n*k) 最差O(n*k) 平均O(n*k);空间复杂度:O(n+kl)

基数排序vs计数排序vs桶排序

  这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异;

  基数排序:根据键值的每位数字来分配桶

  计数排序:每个桶只存储单一键值

  桶排序:每个桶存储一定范围的数值

posted @ 2023-02-07 14:59  梅晓煜  阅读(118)  评论(0)    收藏  举报