日常操作中常见的排序方法有:冒泡排序、快速排序、选择排序、插入排序、希尔排序,甚至还有基数排序、归并排序、二分排序、堆排序、计数排序等。
以下常见算法的定义:
- 插入排序:插入排序基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。插入排序的基本思想是:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
- 选择排序:选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
选择排序是不稳定的排序方法。- 冒泡排序:冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端。
- 快速排序:快速排序(Quicksort)是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 归并排序:归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
- 希尔排序:希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
以下常见算法(上代码)
一、冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
/**
* 冒泡排序(大的值从前往后冒泡)
* 比较相邻的元素。如果第一个比第二个小,就交换他们两个。
* 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的
元素应该会是最小的数。
* 针对所有的元素重复以上的步骤,除了最后一个。
* 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
* 优点:稳定排序;适用于数组存储的数据和链表存储的数据;
*/
public static int[] bubbleSort(int[] a) {
for (int end = a.length - 1; end > 0; end--) {
boolean flag = false; //增加一个判断是否发生过交换的标记
for (int j = 0; j < end; j++) {
if (a[j] > a[j + 1]) {
swap(a, j, j + 1);
flag = true;
}
}
if (!flag) { //如果扫描一遍发现没有发生交换则说明序列已经有序,退出循环
break;
}
}
return a;
}
/**
* 冒泡排序(小的值从后往前下沉)
* 优点:稳定排序;适用于数组存储的数据和链表存储的数据;
*/
public static int[] bubbleSort2(int[] a) {
for (int start = 0; start < a.length - 1; start++) {
boolean flag = false; //增加一个判断是否发生过交换的标记
for (int j = a.length - 1; j > start; j--) {
if (a[j] < a[j - 1]) {
swap(a, j, j - 1);
flag = true;
}
}
if (!flag) { //如果扫描一遍发现没有发生交换则说明序列已经有序,退出循环
break;
}
}
return a;
}
二、快速排序
快速排序使用分治法策略来把一个序列分为两个子序列。
/**
* 快速排序<br/>
* <ul>
* <li>从数列中挑出一个元素,称为“基准”</li>
* <li>重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分割之后,
* 该基准是它的最后位置。这个称为分割(partition)操作。</li>
* <li>递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。</li>
* </ul>
*/
/**
* 快速排序
*
* 从数列中挑出一个元素,称为“基准”
* 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分割之后,
* 该基准是它的最后位置。这个称为分割(partition)操作。
* 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
*/
public static int[] quickSort(int[] a) {
if (a.length > 0) {
quickSortRecursion(a, 0, a.length - 1);
}
return a;
}
public static void quickSortRecursion(int[] data, int low, int high) {
if (low < high) {
int middle = partition(data, low, high);
quickSortRecursion(data, low, middle - 1);
quickSortRecursion(data, middle + 1, high);
}
}
public static int partition(int[] data, int low, int high) {
int temp = data[low]; // 数组的第一个作为中轴
while (low < high) {
while (low < high && data[high] >= temp) {
high--;
}
data[low] = data[high]; // 比中轴小的记录移到低端
while (low < high && data[low] <= temp) {
low++;
}
data[high] = data[low]; // 比中轴大的记录移到高端
}
data[low] = temp;
return low; // 返回中轴的位置
}
/**
* 快速排序的第二种写法
*/
public static int[] quickSort2(int[] a) {
qSort(a, 0, a.length - 1);
return a;
}
public static void qSort(int[] sequence, int low, int high) {
int pivot = sequence[low]; // 取首元素的为基准
int left = low, right = high;
if (low >= high) {
return;
}
swap(sequence, low, high); //将基准与最后一个元素交换
while (true) {
//将序列中比基准小的移到基准左边,比基准大的移到基准右边
while (low < high && sequence[low] <= pivot) {
low++;
}
while (low < high && sequence[high] >= pivot) {
high--;
}
if (low < high) {
swap(sequence, low, high);
} else {
break;
}
}
swap(sequence, low, right); //将最后的基准换到正确的位置
//分别对两个子集进行快排
qSort(sequence, left, low - 1);
qSort(sequence, low + 1, right);
}
三、选择排序
选择排序是一种简单直观的排序方法,每次寻找序列中的最小值,然后放在最末尾的位置。
/**
* 选择排序
* 在未排序序列中找到最小元素,存放到排序序列的起始位置
* 再从剩余未排序元素中继续寻找最小元素,然后放到排序序列起始位置。
* 以此类推,直到所有元素均排序完毕。
* /
public static int[] selectionSort(int[] a) {
for (int i = 0; i < a.length; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[i] > a[j]) {
swap(a, i, j);
}
}
System.out.println(Arrays.toString(a));
}
return a;
}
四、插入排序
插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。其具体步骤参见代码及注释。
/**
* 插入排序
*
* 从第一个元素开始,该元素可以认为已经被排序
* 取出下一个元素,在已经排序的元素序列中从后向前扫描
* 如果该元素(已排序)大于新元素,将该元素移到下一位置
* 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
* 将新元素插入到该位置中
* 重复步骤2
*/
public static int[] insertSort(int[] a) {
for (int i = 1; i < a.length; i++) {
int temp = a[i];
int j = i;
while (j > 0 && temp < a[j - 1]) {
a[j] = a[j - 1];
j--;
}
a[j] = temp;
}
return a;
}
五、归并排序
归并排序是建立在归并操作上的一种有效的排序算法,归并是指将两个已经排序的序列合并成一个序列的操作。
/**
* 归并排序
*
* 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
* 设定两个指针,最初位置分别为两个已经排序序列的起始位置
* 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
* 重复步骤3直到某一指针达到序列尾
* 将另一序列剩下的所有元素直接复制到合并序列尾
* /
public static int[] mergingSort(int[] a) {
if (a.length > 0) {
mergingSortRecursion(a, 0, a.length - 1);
}
return a;
}
public static void mergingSortRecursion(int[] data, int left, int right) {
if (left < right) {
int middle = (left + right) / 2;
mergingSortRecursion(data, left, middle);
mergingSortRecursion(data, middle + 1, right);
merge(data, left, middle, right);
}
}
public static void merge(int[] data, int left, int middle, int right) {
int[] tempArray = new int[data.length];
int i = left; // 左边序列的游标
int j = middle + 1; // 右边序列的游标
int k = left; // 临时序列的游标
// 从两个数组中取出最小的放入中间数组
while (i <= middle && j <= right) {
if (data[i] <= data[j]) {
tempArray[k++] = data[i++];
} else {
tempArray[k++] = data[j++];
}
}
// 剩余部分依次放入中间数组
while (j <= right) {
tempArray[k++] = data[j++];
}
while (i <= middle) {
tempArray[k++] = data[i++];
}
// 将中间数组中的内容复制回原数组
while (left <= right) {
data[left] = tempArray[left++];
}
}
六、二分排序
二分排序是指利用二分法的思想对插入排序进行改进的一种插入排序算法,不同于二叉排序,可以利用数组的特点快速定位指定索引的元素 。
/**
* 二分排序
* 也称折半插入排序,查找次数为O(n log n),移动次数为O(n^2)
* Time complexity: O(n^2)
* 稳定性:稳定排序
*/
public static int[] binarySort(int[] a) {
int i, j;
int low, high, mid;
int temp;
for (i = 1; i < a.length; i++) {
temp = a[i];
low = 0;
high = i - 1;
while (low <= high) {
mid = (low + high) / 2;
if (a[mid] > temp) {
high = mid - 1;
} else {
low = mid + 1;
}
}
for (j = i - 1; j > high; j--) {
a[j + 1] = a[j];
}
a[high + 1] = temp;
}
return a;
}
七、希尔排序
希尔排序是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
/**
* 希尔排序
*/
public static int[] shellSort(int[] a) {
int gap = a.length / 2;
while (gap >= 1) {
for (int i = gap; i < a.length; i++) {
int j;
int temp = a[i];
for (j = i - gap; j >= 0 && temp < a[j]; j = j - gap) {
a[j + gap] = a[j];
}
a[j + gap] = temp;
}
gap /= 2;
}
return a;
}
八、堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
/**
* 堆排序
*/
public static int[] heapSort(int[] a) {
buildMaxHeap(a, a.length - 1);
swap(a, 0, a.length - 1);
for (int i = 1; i < a.length - 1; i++) {
adjustMaxHeap(a, 0, a.length - 1 - i);
swap(a, 0, a.length - 1 - i);
}
return a;
}
public static void buildMaxHeap(int[] data, int lastIndex) {
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
adjustMaxHeap(data, i, lastIndex);
}
}
public static void adjustMaxHeap(int[] data, int parent, int lastIndex) {
/*
* 通常堆是通过一维数组来实现的。在数组起始位置为 0 的情形中:
* 父节点 i 的左子节点在位置 (2*i+1);
* 父节点 i 的右子节点在位置 (2*i+2);
* 子节点 i 的父节点在位置 floor((i-1)/2);
*/
while (2 * parent + 1 <= lastIndex) {
int maxChildIndex = 2 * parent + 1;
// 如果当前左孩子不是末尾元素
if (maxChildIndex < lastIndex) {
// 如果左孩子小于右孩子,取右孩子下标
if (data[maxChildIndex] < data[maxChildIndex + 1]) {
maxChildIndex++;
}
}
// 比较当前父节点和最大孩子节点
if (data[parent] < data[maxChildIndex]) {
swap(data, parent, maxChildIndex);
parent = maxChildIndex;
} else {
break;
}
}
}
public static void swap(int[] data, int i, int j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
九、基数排序
基数排序是属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,
其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积
的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
/**
* 基数排序
*/
public static int[] radixSort(int[] a) {
int max = 0;
for (int i = 0; i < a.length; i++) {
max = a[i] > max ? a[i] : max;
}
int time = 0;
while (max > 0) {
time++;
max /= 10;
}
List<ArrayList<Integer>> queue = new ArrayList<>();
for (int i = 0; i < 10; i++) {
queue.add(new ArrayList<>());
}
for (int i = 0; i < time; i++) {
// 按某位对原数组进行一趟排序
for (int j = 0; j < a.length; j++) {
int d = a[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
ArrayList<Integer> list = queue.get(d);
list.add(a[j]);
queue.set(d, list);
}
// 把queue进行过一趟排序的数据拷贝回原数组
int count = 0;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
a[count] = queue.get(k).get(0);
queue.get(k).remove(0);
count++;
}
}
}
return a;
}
十、计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。
/**
* 计数排序法1 取出序号用了少量的比较和循环
*/
public static int[] countingSort1(int[] a) {
int max = 0;
for (int i = 0; i < a.length; i++) {
max = a[i] > max ? a[i] : max;
}
int[] count = new int[max + 1];
for (int i = 0; i < a.length; i++) {
count[a[i]]++;
}
int sum = 0;
for (int i = 0; i < count.length; i++) {
if (count[i] > 0) {
for (int j = 0; j < count[i]; j++) {
a[sum + j] = i;
}
}
sum += count[i];
}
return a;
}
/**
* 计数排序法2 完全没有使用比较和循环
*/
public static int[] countingSort2(int[] a) {
int max = 0;
for (int i = 0; i < a.length; i++) {
max = a[i] > max ? a[i] : max;
}
int[] count = new int[max + 1];
for (int i = 0; i < a.length; i++) {
count[a[i]]++;
}
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
int[] b = new int[a.length];
for (int i = 0; i < a.length; i++) {
b[count[a[i]] - 1] = a[i];
count[a[i]]--;
}
return b;
}
各个方法的测试代码实现如下:
public static void main(String[] args) {
int a[] = {49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 56, 17, 18, 23, 34, 15, 35,
25, 53, 51};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(binarySort(a)));
}
运行结果如下:
[49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 5, 4, 62, 99, 98, 54, 56, 17, 18, 23, 34, 15, 35, 25, 53, 51]
[4, 5, 12, 13, 15, 17, 18, 23, 25, 27, 34, 34, 35, 38, 49, 49, 51, 53, 54, 56, 62, 64, 65, 76, 78, 97, 98, 99]
Process finished with exit code 0