关于排序:算法十大经典排序算法动画演示

42次阅读

共计 7759 个字符,预计需要花费 20 分钟才能阅读完成。

(PS:原博客戳这里。原博主写的太好了,所以间接转载过去。为了本人可能学习分明,我将代码局部删掉替换成本人写的代码,不便当前查看。)

0、算法概述

0.1 算法分类

十种常见排序算法能够分为两大类:

  • 比拟类排序:通过比拟来决定元素间的绝对秩序,因为其工夫复杂度不能冲破 O(nlogn),因而也称为非线性工夫比拟类排序。
  • 非比拟类排序:不通过比拟来决定元素间的绝对秩序,它能够冲破基于比拟排序的工夫下界,以线性工夫运行,因而也称为线性工夫非比拟类排序。

0.2 算法复杂度

0.3 相干概念

  • 稳固:如果 a 本来在 b 后面,而 a =b,排序之后 a 依然在 b 的后面。
  • 不稳固:如果 a 本来在 b 的后面,而 a =b,排序之后 a 可能会呈现在 b 的前面。
  • 工夫复杂度:对排序数据的总的操作次数。反映当 n 变动时,操作次数出现什么法则。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模 n 的函数。

1、冒泡排序(Bubble Sort)

冒泡排序是一种简略的排序算法。它反复地走访过要排序的数列,一次比拟两个元素,如果它们的程序谬误就把它们替换过去。走访数列的工作是反复地进行直到没有再须要替换,也就是说该数列曾经排序实现。这个算法的名字由来是因为越小的元素会经由替换缓缓“浮”到数列的顶端。

1.1 算法形容

  • 比拟相邻的元素。如果第一个比第二个大,就替换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最初一对,这样在最初的元素应该会是最大的数;
  • 针对所有的元素反复以上的步骤,除了最初一个;
  • 反复步骤 1~3,直到排序实现。

1.2 动图演示

1.3 代码实现

  public static void bubbleSort(int[] array) {for (int i = 0; i < array.length; i++) {for (int j = 0; j < array.length - 1 - i; j++) {if (array[j + 1] < array[j]) {int tmp = array[j + 1];
          array[j + 1] = array[j];
          array[j] = tmp;
        }
      }
    }
  }

2、抉择排序(Selection Sort)

抉择排序 (Selection-sort) 是一种简略直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,寄存到排序序列的起始地位,而后,再从残余未排序元素中持续寻找最小(大)元素,而后放到已排序序列的开端。以此类推,直到所有元素均排序结束。

2.1 算法形容

n 个记录的间接抉择排序可通过 n - 1 趟间接抉择排序失去有序后果。具体算法形容如下:

  • 初始状态:无序区为 R\[1..n\],有序区为空;
  • 第 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 个的新无序区;
  • n- 1 趟完结,数组有序化了。

2.2 动图演示

2.3 代码实现

  public static void selectionSort(int[] array) {for(int i=0;i<array.length-1;i++){
          int flag=i;
          int tmp;
          for(int j=i+1;j<array.length;j++){if(array[j]<array[flag]){flag=j;}
          }
          tmp=array[i];
          array[i]=array[flag];
          array[flag]=tmp;
      }
  }

2.4 算法剖析

体现最稳固的排序算法之一,因为无论什么数据进去都是 O(n2)的工夫复杂度,所以用到它的时候,数据规模越小越好。惟一的益处可能就是不占用额定的内存空间了吧。实践上讲,抉择排序可能也是平时排序个别人想到的最多的排序办法了吧。

3、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法形容是一种简略直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应地位并插入。

3.1 算法形容

一般来说,插入排序都采纳 in-place 在数组上实现。具体算法形容如下:

  • 从第一个元素开始,该元素能够认为曾经被排序;
  • 取出下一个元素,在曾经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一地位;
  • 反复步骤 3,直到找到已排序的元素小于或者等于新元素的地位;
  • 将新元素插入到该地位后;
  • 反复步骤 2~5。

3.2 动图演示

3.2 代码实现

  public static void insertionSort(int[] array) {for (int i = 1; i < array.length; i++) {int cur = array[i];
      for (int j = i - 1; j >= 0; j--) {if (cur < array[j]) {array[j + 1] = array[j];
        } else {array[j + 1] = cur;
          break;
        }
      }
    }
  }

3.4 算法剖析

插入排序在实现上,通常采纳 in-place 排序(即只需用到 O(1)的额定空间的排序),因此在从后向前扫描过程中,须要重复把已排序元素逐渐向后挪位,为最新元素提供插入空间。

4、希尔排序(Shell Sort)

1959 年 Shell 创造,第一个冲破 O(n2)的排序算法,是简略插入排序的改进版。它与插入排序的不同之处在于,它会优先比拟间隔较远的元素。希尔排序又叫 放大增量排序

4.1 算法形容

先将整个待排序的记录序列宰割成为若干子序列别离进行间接插入排序,具体算法形容:

  • 抉择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • 每趟排序,依据对应的增量 ti,将待排序列宰割成若干长度为 m 的子序列,别离对各子表进行间接插入排序。仅增量因子为 1 时,整个序列作为一个表来解决,表长度即为整个序列的长度。

4.2 动图演示

4.3 代码实现

  public static void shellSort(int[] array){
      int len=array.length;
      for(int gap=len/2;gap>=1;gap=gap/2){for(int i=0;i<len-gap;i++){if(array[i]>=array[i+gap]){int tmp=array[i];
                  array[i]=array[i+gap];
                  array[i+gap]=tmp;
              }
          }
      }
  }

4.4 算法剖析

希尔排序的外围在于距离序列的设定。既能够提前设定好距离序列,也能够动静的定义距离序列。动静定义距离序列的算法是《算法(第 4 版)》的合著者 Robert Sedgewick 提出的。

5、归并排序(Merge Sort)

归并排序是建设在归并操作上的一种无效的排序算法。该算法是采纳分治法(Divide and Conquer)的一个十分典型的利用。将已有序的子序列合并,失去齐全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2 - 路归并。

5.1 算法形容

  • 把长度为 n 的输出序列分成两个长度为 n / 2 的子序列;
  • 对这两个子序列别离采纳归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.2 动图演示

5.3 代码实现

  public static void mergeSort(int[] array, int start, int end){if(start<end){int mid=(start+end)/2;
          mergeSort(array,start,mid);
          mergeSort(array,mid+1,end);
          merge(array,start,mid,end);
      }
  }
  public static void merge(int[] array, int start, int mid, int end){
      int p1=start;
      int p2=mid+1;
      int k=start;
      int[] tmp=new int[array.length];
      while (p1<=mid&&p2<=end){if(array[p1]<array[p2]){tmp[k++]=array[p1++];
          }else {tmp[k++]=array[p2++];
          }
      }
      while (p1<=mid){tmp[k++]=array[p1++];
      }
      while (p2<=end){tmp[k++]=array[p2++];
      }
      for(int i=start;i<=end;i++){array[i]=tmp[i];
      }
  }

5.4 算法剖析

归并排序是一种稳固的排序办法。和抉择排序一样,归并排序的性能不受输出数据的影响,但体现比抉择排序好的多,因为始终都是 O(nlogn)的工夫复杂度。代价是须要额定的内存空间。

6、疾速排序(Quick Sort)

疾速排序的根本思维:通过一趟排序将待排记录分隔成独立的两局部,其中一部分记录的关键字均比另一部分的关键字小,则可别离对这两局部记录持续进行排序,以达到整个序列有序。

6.1 算法形容

疾速排序应用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法形容如下:

  • 从数列中挑出一个元素,称为“基准”(pivot);
  • 从新排序数列,所有元素比基准值小的摆放在基准后面,所有元素比基准值大的摆在基准的前面(雷同的数能够到任一边)。在这个分区退出之后,该基准就处于数列的两头地位。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.2 动图演示

6.3 代码实现

  public static void quickSort(int[] array, int low, int high) {
    int p = low;
    int q = high;
    if (low < high) {int tmp = array[low];
      while (p < q) {while (p < q && array[q] > tmp) {q--;}
        if (p < q) {array[p] = array[q];
          p++;
        }
        while (p < q && array[p] < tmp) {p++;}
        if (p < q) {array[q] = array[p];
          q--;
        }
      }
      array[p] = tmp;
      quickSort(array, low, p - 1);
      quickSort(array, p + 1, high);
    } else {return;}
  }

7、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。沉积是一个近似齐全二叉树的构造,并同时满足沉积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1 算法形容

  • 将初始待排序关键字序列 (R1,R2….Rn) 构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素 R\[1\]与最初一个元素 R\[n\]替换,此时失去新的无序区 (R1,R2,……Rn-1) 和新的有序区(Rn), 且满足 R\[1,2…n-1\]<=R\[n\];
  • 因为替换后新的堆顶 R\[1\]可能违反堆的性质,因而须要对以后无序区 (R1,R2,……Rn-1) 调整为新堆,而后再次将 R\[1\]与无序区最初一个元素替换,失去新的无序区 (R1,R2….Rn-2) 和新的有序区(Rn-1,Rn)。一直反复此过程直到有序区的元素个数为 n -1,则整个排序过程实现。

7.2 动图演示

7.3 代码实现

class Solution {public int[] sortArray(int[] nums) {heapSort(nums, nums.length - 1);
        return nums;
    }

    /**
     * 堆排序入口
     * @param nums
     * @param n
     */
    private void heapSort(int[] nums, int n) {
        // 建堆
        for (int i = n; i >= 0; i--) {heapify(nums, n, i);
        }
        // 排序
        for (int i = n; i > 0; i--) {swap(nums, i, 0);
            heapify(nums, i - 1, 0);
        }
    }

    /**
     * 保护堆
     * @param nums
     * @param n
     * @param i
     */
    private void heapify(int[] nums, int n, int i) {
        int largest = i;
        int l = 2 * i + 1;
        int r = 2 * i + 2;
        if (l <= n && nums[l] > nums[largest]) {largest = l;}
        if (r <= n && nums[r] > nums[largest]) {largest = r;}
        if(largest!=i){swap(nums, largest, i);
            heapify(nums, n, largest);
        }
    }

    /**
     * 替换元素
     *
     * @param nums
     * @param i
     * @param j
     */
    private void swap(int[] nums, int i, int j) {int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

}

8、计数排序(Counting Sort)

计数排序不是基于比拟的排序算法,其外围在于将输出的数据值转化为键存储在额定开拓的数组空间中。作为一种线性工夫复杂度的排序,计数排序 要求输出的数据必须是有确定范畴的整数

8.1 算法形容

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为 i 的元素呈现的次数,存入数组 C 的第 i 项;
  • 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加);
  • 反向填充指标数组:将每个元素 i 放在新数组的第 C(i)项,每放一个元素就将 C(i)减去 1。

8.2 动图演示

8.3 代码实现

public int[] countSort(int[] nums) {
        int max = Integer.MIN_VALUE;
        for (int num : nums) {max = max > num ? max : num;}
        int[] count = new int[max + 1];
        for (int num : nums) {count[num]++;
        }
        int[] ans = new int[nums.length];
        int index = 0;
        for (int i = 0; i < count.length; i++) {for (int j = 0; j < count[i]; j++) {ans[index++] = i;
            }
        }
        return ans;
    }

8.4 算法剖析

计数排序是一个稳固的排序算法。当输出的元素是 n 个 0 到 k 之间的整数时,工夫复杂度是 O(n+k),空间复杂度也是 O(n+k),其排序速度快于任何比拟排序算法。当 k 不是很大并且序列比拟集中时,计数排序是一个很无效的排序算法。

9、桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的要害就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假如输出数据遵从均匀分布,将数据分到无限数量的桶里,每个桶再别离排序(有可能再应用别的排序算法或是以递归形式持续应用桶排序进行排)。

9.1 算法形容

  • 设置一个定量的数组当作空桶;
  • 遍历输出数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

9.2 图片演示

9.3 代码实现

public int[] bucketSort(int[] nums) {
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int num : nums) {
            max = max > num ? max : num;
            min = min < num ? min : num;
        }
        int bucket = (max - min) / nums.length + 1;
        ArrayList<ArrayList<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i < bucket; i++) {buckets.add(new ArrayList<Integer>());
        }
        for (int num : nums) {int index = (num - min) / nums.length;
            buckets.get(index).add(num);
        }
        int[] ans = new int[nums.length];
        int k = 0;
        for (int i = 0; i < buckets.size(); i++) {Collections.sort(buckets.get(i));
            for (int j = 0; j < buckets.get(i).size(); j++) {ans[k++] = buckets.get(i).get(j);
            }
        }
        return ans;
    }

9.4 算法剖析

桶排序最好状况下应用线性工夫 O(n),桶排序的工夫复杂度,取决与对各个桶之间数据进行排序的工夫复杂度,因为其它局部的工夫复杂度都为 O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的工夫也会越少。但相应的空间耗费就会增大。

10、基数排序(Radix Sort)

基数排序是依照低位先排序,而后收集;再依照高位排序,而后再收集;顺次类推,直到最高位。有时候有些属性是有优先级程序的,先按低优先级排序,再按高优先级排序。最初的秩序就是高优先级高的在前,高优先级雷同的低优先级高的在前。

10.1 算法形容

  • 获得数组中的最大数,并获得位数;
  • arr 为原始数组,从最低位开始取每个位组成 radix 数组;
  • 对 radix 进行计数排序(利用计数排序实用于小范畴数的特点);

10.2 动图演示

 

10.3 代码实现

    public int[] radixSort(int[] nums) {
        int max = Integer.MIN_VALUE;
        for (int num : nums) {max = max > num ? max : num;}
        int key = 0;
        while (max > 0) {
            max /= 10;
            key++;
        }

        ArrayList<ArrayList<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i < 10; i++) {buckets.add(new ArrayList<>());
        }
        for (int i = 0; i < key; i++) {for (int j = 0; j < nums.length; j++) {int k = nums[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
                buckets.get(k).add(nums[j]);
            }
            int index = 0;
            for (int m = 0; m < 10; m++) {int size = buckets.get(m).size();
                for (int n = 0; n < size; n++) {nums[index++] = buckets.get(m).remove(0);
                }
            }
        }
        return nums;
    }

10.4 算法剖析

基数排序基于别离排序,别离收集,所以是稳固的。但基数排序的性能比桶排序要略差,每一次关键字的桶调配都须要 O(n)的工夫复杂度,而且调配之后失去新的关键字序列又须要 O(n)的工夫复杂度。如果待排数据能够分为 d 个关键字,则基数排序的工夫复杂度将是 O(d\*2n),当然 d 要远远小于 n,因而基本上还是线性级别的。

基数排序的空间复杂度为 O(n+k),其中 k 为桶的数量。一般来说 n >>k,因而额定空间须要大略 n 个左右。

正文完
 0