排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
用一张图概括:
名词解释:
n
:数据规模
k
:”桶”的个数
In-place
:占用常数内存,不占用额外内存
Out-place
:占用额外内存
稳定性
:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
内部排序 |
描述 |
冒泡排序 |
(无序区,有序区) 从无序区通过交换找出最大元素放到有序区最前端。 |
选择排序 |
(无序区,有序区) 从无序区选择一个最小的元素放到有序区的末尾。比较多,交换少 |
插入排序 |
(无序区,有序区) 把无序区的第一个元素插入到有序区合适的位置。比较少,交换多 |
希尔排序 |
通过比较相距一定间隔的元素来进行(每一定间隔取一个元素组成子序列),各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。 |
归并排序 |
把数组从中间分成前后两部分,分别排序,再将排好序的两部分合并。 |
快速排序 |
(小数,基准,大数) 随机一个元素作为基准数,将小于基准的元素放在基准之前,大于基准的放在基准之后,再分别对小数区与大数区进行排序。 |
堆排序 |
(大顶堆,有序区) 取出堆顶元素放在有序区,再恢复堆。 |
计数排序 |
统计小于等于该元素值的元素个数i,于是该元素就放在目标数组的索引i位(i>=0)。 |
桶排序 |
将值为i的元素放在i号桶,最后依次把桶里元素倒出来。 |
基数排序 |
一种多关键字的排序算法,可用桶排序实现。 |
1. 冒泡排序(Bubble Sort)
冒泡排序(Bubble Sort)
是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
- 冒泡排序步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public void bubbleSort(int[] arr, int len){ if (len <= 1) return; for (int i = 0; i < len; i++) { boolean flag = false; for (int j = 0; j < len-i; j++) { if(arr[j] > arr[j+1]){ int temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; flag = true; } } if (!flag){ break; } } }
|
2. 选择排序(Selection sort)
选择排序(Selection sort)
是一种简单直观的排序算法。 它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。 以此类推,直到全部待排序的数据元素的个数为零。 选择排序是不稳定的排序方法。
- 冒泡排序步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public void selectionSort(int[] arr, int len){ if (len <= 1) return; for (int i = 0; i < len-1; i++) { int min=i; for (int j = i+1; j < len; j++) { if(arr[j] < arr[min]){ min = j; } } if (i != min){ int temp = arr[i]; arr[i] = arr[min]; arr[min] = temp; } } }
|
3. 插入排序(Insertion Sort)
插入排序(Insertion Sort)
是一种最简单直观的排序算法,一般也被称为直接插入排序。
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
- 插入排序步骤:
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public void insertionSort(int[] arr, int len){ if (len <= 1 ) return; for (int i = 1; i < len; i++) { int temp = arr[i]; int j = i-1; for (; j >=0; j--) { if (arr[j] > temp){ arr[j+1] = arr[j]; } else { break; } } arr[j+1] = temp; } }
|
4. 希尔排序(Shell Sort)
希尔排序(Shell Sort)
是插入排序的一种更高效的改进版本;又称缩小增量排序,因 DL.Shell 于 1959 年提出而得名。
通过比较相距一定间隔的元素来进行(每一定间隔取一个元素组成子序列),各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。
- 希尔排序步骤:
- 将 len 个元素的数组序列 分割为间隔 gap (gap = len/2) 的子序列(每间隔 gap 取一个元素)
- 在每一个子序列中分别采用直接插入排序。
- 然后缩小间隔 gap,将 gap = gap/2。
- 重复上述的子序列划分和排序工作,随着序列减少最后变为一个,也就完成了整个排序。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public void shellSort(int[] arr, int len){ if (len <= 1) return; for (int gap = len/2; gap > 0; gap = gap/2) { for (int i = gap; i < len; i++) { int temp = arr[i]; int j = i-gap; for (; j >=0; j = j-gap) { if (arr[j] > temp){ arr[j+gap] = arr[j]; } else { break; } } arr[j+gap] = temp; } } }
|
5. 归并排序(Merge sort)
归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序使用的就是分治思想。分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧,这两者并不冲突。
- 归并排序步骤:
- 先把数组从中间分成前后两部分子序列,然后对前后两部分分别排序,然后再合并。
- 前后两部分子序列递归重复上述步骤。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
public void mergeSort(int[] arr, int len) { mergeSortRecursion(arr, 0, len-1); }
private void mergeSortRecursion(int[] arr, int p, int r) { if (p == r) return; int q = p + (r-p)/2; mergeSortRecursion(arr, p, q); mergeSortRecursion(arr, q+1, r); mergeResult(arr, p, q, r); }
private void mergeResult(int[] arr, int p, int q, int r) { int[] temp = new int[r-p+1]; int i = p; int j = q +1; int k = 0; while (i <= q && j <= r) { if (arr[i] <= arr[j]) { temp[k] = arr[i]; i++; } else { temp[k] = arr[j]; j++; } k++; } int start = i, end = q; if (j <= r) { start = j; end = r; } while (start <= end) { temp[k++] = arr[start++]; } System.arraycopy(temp, 0, arr, p, temp.length); }
|
6. 快速排序(Quick Sort)
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
排序算法的思想非常简单,在待排序的数列中,我们首先要找一个数字作为基准数。把小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了;接着把两个分区的元素分别按照上面两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。
- 快速排序步骤:
- 从数列中挑出一个元素,称为 “基准”(pivot)。
- 将小于基准的元素放在基准之前,大于基准的放在基准之后,再分别对小数区与大数区进行快速排序。
- 小数区与大数区子序列递归地重复上述步骤(直到分区子序列的大小是0或1)。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
public void quickSort(int[] arr, int len) { quickSortRecursion(arr, 0, len-1); }
private void quickSortRecursion(int[] arr, int p, int r) { if (p >= r) return; int q = getPartitionIndex(arr, p, r); System.out.println("pivotIndex=: " + q + ",pivot=" + arr[q]); System.out.println(Arrays.toString(Arrays.copyOfRange(arr, p, r+1))); quickSortRecursion(arr, p, q-1); quickSortRecursion(arr, q+1, r); }
private int getPartitionIndex(int[] arr, int p, int r) { int pivot = arr[r]; int i = p; for (int j = p; j < r; j++) { if (arr[j] < pivot) { swapArr(arr, i, j); i++; } } swapArr(arr, i, r); return i; }
private void swapArr(int[] arr, int a, int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; }
|
7. 堆排序(Heap Sort)
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
public void heapSort(int[] arr, int len) { buildHeap(arr, len); int k = len; while (k > 1) { swapArr(arr, 1, k); --k; heapify(arr, k, 1); } }
private void buildHeap(int[] arr, int len) { for (int i = len/2; i >= 1; --i) { heapify(arr, len, i); } }
private void heapify(int[] arr, int len, int i) { while (true) { int maxPos = i; if (i*2 <= len && arr[i] < arr[i*2]) maxPos = i*2; if (i*2+1 <= len && arr[maxPos] < arr[i*2+1]) maxPos = i*2+1; if (maxPos == i) break; swapArr(arr, i, maxPos); i = maxPos; } }
private void swapArr(int[] arr, int a, int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; }
|
8. 计数排序(Counting Sort)
计数排序(Counting Sort)是一种牺牲内存空间来换取低时间复杂度的排序算法,同时它也是一种不基于比较的算法。
它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)
(其中k是整数的范围),快于任何比较排序算法。
它适合于最大值和最小值的差值不是不是很大的排序。
基本思想:就是把数组元素作为数组的下标,然后用一个临时数组统计该元素出现的次数,例如 temp[i] = m, 表示元素 i 一共出现了 m 次。最后再把临时数组统计的数据从小到大汇总起来,此时汇总起来是数据是有序的。
- 计数排序步骤:
- 找出待排序数组中最大值
max
和最小值 min
;
- 创建临时数组,大小为
(max - min + 1)
;
- 统计待排序数组中每个值为
i
的元素出现的次数,存入临时数组的第i
项;
- 反向填充目标数组:依次从临时数组中取出放入新数组。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
public void countingSort(int[] arr, int len) { if (len <= 1 ) return; int min = getMin(arr); int max = getMax(arr); int range = max - min + 1; int[] temp = new int[range]; for (int i = 0; i < len; i++) { temp[arr[i] - min]++; } int k = 0; for (int i = 0; i < range; i++) { for (int j = temp[i]; j > 0; j--) { arr[k++] = i + min; } } }
private int getMax(int[] arr) { int max = arr[0]; for (int value : arr) { if (max < value) { max = value; } } return max; }
private int getMin(int[] arr) { int min = arr[0]; for (int value : arr) { if (min > value) { min = value; } } return min; }
|
9. 桶排序(Bucket Sort)
桶排序(Bucket sort)是计数排序的升级版。核心思想是将要排序的数据分到几个有序的“桶”里,每个桶里的数据再单独进行排序。
桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
桶排序的表现取决于数据的分布。当数据在各个桶之间的分布是比较均匀的,排序效率非常高;在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public void bucketSort(int[] arr, int len) { if (len <= 1 ) return; int min = getMin(arr); int max = getMax(arr); int range = max - min; int bucketNum = range / 5 + 1; ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(bucketNum); for (int i = 0; i < bucketNum; i++) { bucketList.add(new LinkedList<Integer>()); } for (int a : arr) { bucketList.get((a - min) / range).add(a - min); } for (int i = 0; i < bucketNum; i++) { Collections.sort(bucketList.get(i)); } int k = 0; for (int i = 0; i < bucketNum; i++) { for (Integer t : bucketList.get(i)) { arr[k++] = t + min; } } }
|
10. 基数排序(Radix Sort)
基数排序(Radix Sort)要求数据可以划分成高低位,位之间有递进关系。比较两个数,我们只需要比较高位,高位相同的再比较低位。
而且每一位的数据范围不能太大,因为基数排序算法需要借助桶排序或者计数排序来完成每一个位的排序工作。
基数排序的方式可以采用最低位优先LSD(Least sgnificant digital)法或最高位优先MSD(Most sgnificant digital)法,LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
- 基数排序步骤:
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。
- 然后,从最低位开始,依次进行一次排序。
- 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
public void radixSort(int[] arr, int len) { if (len <= 1 ) return; int num = getMaxNum(arr); ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10); for (int i = 0; i < 10; i++) { bucketList.add(new LinkedList<Integer>()); } for (int i = 1; i <= num; i++) { for (int j = 0; j < len; j++) { int radio = (arr[j] / (int)Math.pow(10,i-1)) % 10; bucketList.get(radio).add(arr[j]); } int k = 0; for (int j = 0; j < 10; j++) { for (Integer t : bucketList.get(j)) { arr[k++] = t; } bucketList.get(j).clear(); } } }
private int getMaxNum(int[] arr) { int max = arr[0]; for (int value : arr) { if (max < value) { max = value; } } int num = 1; while (max / 10 > 0) { num++; max = max / 10; } return num; }
|
原文链接: http://chaooo.github.io/2022/05/25/data-structure-sort.html
版权声明: 转载请注明出处.