常见排序算法(百度百科)
排序法 | 最差时间分析 | 平均时间复杂度 | 稳定度 | 空间复杂度 |
冒泡排序 | O(n2) | O(n2) | 稳定 | O(1) |
快速排序 | O(n2) | O(n*log2n) | 不稳定 | O(log2n)~O(n) |
选择排序 | O(n2) | O(n2) | 稳定 | O(1) |
二叉树排序 | O(n2) | O(n*log2n) | 不一顶 | O(n) |
插入排序 | O(n2) | O(n2) | 稳定 | O(1) |
堆排序 | O(n*log2n) | O(n*log2n) | 不稳定 | O(1) |
希尔排序 | O | O | 不稳定 | O(1) |
package com.lpj.algorithm;
/**
* @author LPJ
* @version 1.0 创建时间:2018年3月3日 上午9:05:14
* describe:冒泡排序:Bubble
*/
import java.util.Arrays;
public class Bubble {
public static void main(String[] args) {
int[] aa = {2,44,21,4,6,88};
System.out.println(Arrays.toString(aa));
// Arrays.stream(aa).forEach(System.out::println);
aa = bub(aa);
System.out.println(Arrays.toString(aa));
// Arrays.stream(aa).forEach(System.out::println);
}
static int[] bub(int [] arr){
/**
* 原理冒泡排序升序:采用第一个和第二个比较,如果第一个大于第二个,进行交换。然后第二个和第三比较
* 一次类推
* 然后进行第二轮比较,从第一个到n-1个为止
*/
for(int i = 0; i < arr.length -1; i++) {
for(int j = 0; j < arr.length - 1 - i; j ++) {
if (arr[j] > arr[j + 1]) {
// arr[i] = arr[i + 1] + (arr[i + 1] = arr[i]) * 0;//位置交换,代码简单,效率低
int c = 0;
c = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = c;//代码复杂效率高
}
}
}
return arr;
}
}
二、选择排序(java)寻找最大值,然后与最后一个交换
package com.lpj.algorithm;
import java.util.Arrays;
/**
* @author LPJ
* @version 1.0 创建时间:2018年3月3日 上午9:32:50
* describe:选择排序升序:原理,每次循环找出最大值,然后与最后一个元素交换位置
*/
public class Selection {
public static void main(String[] args) {
int[] aa = {2,44,22,1,5,53};
System.out.println(Arrays.toString(aa));
aa = select(aa);
System.out.println(Arrays.toString(aa));
}
static int[] select(int [] arr) {
for(int i = 0; i < arr.length - 1; i++) {
int max = arr.length - 1 - i;
for(int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[max]) {
max = j;
}
}
if (arr.length - 1 - i != max) {
int c = 0;
c = arr[arr.length - i -1];
arr[arr.length - i -1] = arr[max];
arr[max] = c;
}
}
return arr;
}
}
三、插入排序(java),默认在有序表中插入元素,通过比较移动顺序
package com.lpj.algorithm;
import java.util.Arrays;
/**
* @author LPJ
* @version 1.0 创建时间:2018年3月3日 上午10:06:39
* describe:插入排序升序:原理,默认元素有序(开始为第一个元素),后边元素依次和有序元素进行比较
* 如果查找到比当前元素小的元素,则插入其后,否则插在有序数组的最后
*/
public class Insert {
public static void main(String[] args) {
int[] aa = {2,5,21,57,13,22};
System.out.println(Arrays.toString(aa));
aa = insert(aa);
System.out.println(Arrays.toString(aa));
}
static int[] insert(int[] arr) {
for(int i = 1; i < arr.length; i ++) {
int j = i;
int temp = arr[i];
while (j > 0 && temp < arr[j - 1]) {//算法核心,位置交换
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp;
}
return arr;
}
}
四、快速排序(java),采用这是基准值,前后双向比较的方式,按照基准值将元素分为两部分,然后通过递归实现
package com.lpj.algorithm;
import java.util.Arrays;
/**
* @author LPJ
* @version 1.0 创建时间:2018年3月3日 上午11:23:57
* describe:原理:采用单轴双向比较方法,设置基准值,首次循环从后向前对比,当小于基准值时,交换,然后从开始
* 位置与基准值进行比较,当大于基准值时,进行交换。全部结束后,元素分为两部分,右侧为大于基准值,左侧为小于基准值
* 然后将两部分分别递归调用排序方法
*/
public class QucikSort {
public static void main(String[] args) {
int [] aa = {3,2,45,21,5};
System.out.println(Arrays.toString(aa));
aa = quickSort1(aa);
System.out.println(Arrays.toString(aa));
}
static int[] quickSort1(int [] a) {
return quickSort(a, 0, a.length - 1);
}
//快速排序方法
static int[] quickSort(int [] a,int low, int high) {
int start = low;//记录起始位置
int end = high;//记录终点位置
int key = a[low];//记录分界点值,基准值
//首次循环
while(start < end) {
//从后向前比较,如果大于基准值,继续向前
while(start < end && a[end] >= key)
end--;
//如果小于基准值,进行交换
a[start] = a[end] + (a[end] = a[start]) * 0;
//从前向后比较,如果小于基准值,继续向后
while(start < end && a[start] <= key)
start++;
//如果大于基准值,进行交换
a[start] = a[end] + (a[end] = a[start]) * 0;
}
//递归,分成两部分,此时start = end
//基准值以及以前的部分,重复递归end-1
//基准值以及以后的部分,重复递归end+1
if(start > low) quickSort(a, low, end - 1);
if(end < high) quickSort(a, end + 1, high);
return a;
}
}
五、归并排序(java)
package com.lpj.algorithm;
import java.util.Arrays;
/**
* @author LPJ
* @version 1.0 创建时间:2018年3月3日 下午4:11:51
* describe:归并排序:原理:归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:
* 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
* 设定两个指针,最初位置分别为两个已经排序序列的起始位置比较两个指针所指向的元素,
* 选择相对小的元素放入到合并空间,并移动指针到下一位置重复步骤3直到某一指针到达序列尾
* 将另一序列剩下的所有元素直接复制到合并序列尾
*/
public class MergeSort {
public static void main(String[] args) {
int[]aa = { 2,1};
aa = megersort(aa);
System.out.println(Arrays.toString(aa));
}
static int[] megersort(int [] arr) {
int left = 0;
int right = arr.length - 1;
metgesort(arr, left, right);
return arr;
}
static int[] metgesort(int []arr ,int left, int right) {
if (left == right) {
return arr;//当细分结束,返回结果
}
int mid = (left + right) / 2;
metgesort(arr, left, mid);
metgesort(arr, mid + 1, right);
arr = merge(arr, left, mid, right);//细分到最小单位后进行合并
return arr;
}
static int [] merge(int [] A, int left, int mid, int right)// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]
{
int len = right - left + 1;
int[] temp = new int[len]; // 辅助空间O(n)
int index = 0;
int i = left; // 前一数组的起始元素
int j = mid + 1; // 后一数组的起始元素
while (i <= mid && j <= right){
//比较两个值的大小,将小的值放在前面
temp[index++] = A[i] <= A[j] ? A[i++] : A[j++]; // 带等号保证归并排序的稳定性
}
while (i <= mid){
//将剩余值放入合并数组
temp[index++] = A[i++];
}
while (j <= right){
temp[index++] = A[j++];//将剩余值放入合并数组
}
for (int k = 0; k < len; k++){
A[left++] = temp[k];//将合并数组赋给原始数组
}
return A;
}
}
六、堆排序
package com.lpj.algorithm;
import java.util.Arrays;
/**
* @author LPJ
* @version 1.0 创建时间:2018年3月3日 下午5:07:40
* describe:堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构
* (通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,
* 其中父结点的值总是大于它的孩子节点。我们可以很容易的定义堆排序的过程:由输入的无序数组构造一个最大堆,
* 作为初始的无序区把堆顶元素(最大值)和堆尾元素互换把堆(无序区)的尺寸缩小1,
* 并调用heapify(A, 0)从新的堆顶元素开始进行堆调整重复步骤2,直到堆的尺寸为1
*/
public class HeapSort {
static void heapSort(int arr[], int n)
{
int heap_size = buildHeap(arr, n); // 建立一个最大堆
while (heap_size > 1) {
// 堆(无序区)元素个数大于1,未完成排序
// 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
// 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
swap(arr, 0, --heap_size);
heapify(arr, 0, heap_size); // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
}
}
static int buildHeap(int arr[], int n) { // 建堆,时间复杂度O(n)
int heap_size = n;
for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
heapify(arr, i, heap_size);
return heap_size;
}
static void heapify(int arr[], int i, int size) { // 从A[i]向下进行堆调整
int left_child = 2 * i + 1; // 左孩子索引
int right_child = 2 * i + 2; // 右孩子索引
int max = i; // 选出当前结点与其左右孩子三者之中的最大值
if (left_child < size && arr[left_child] > arr[max])
max = left_child;
if (right_child < size && arr[right_child] > arr[max])
max = right_child;
if (max != i){
swap(arr, i, max); // 把当前结点和它的最大(直接)子节点进行交换
heapify(arr, max, size); // 递归调用,继续从当前结点向下进行堆调整
}
}
static void swap(int arr[], int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
static void heapsort (int[] arr) {
int n = arr.length;
heapSort(arr, n);
}
public static void main(String[] args) {
int [] aa = {21,3,12,4,67};
heapsort(aa);
System.out.println(Arrays.toString(aa));
}
}
七、其他简单排序(百度百科)
计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
其中在Hadoop中的BitMap采用的就是计数排序思想的改进版本,数组采用位数组(只存放0和1)。
java的sort排序:
对于基本类型(不稳定排序组合)和引用类型(稳定排序组合)采用不同的方式:
j基本类型采用的是插入排序和快速排序的组合:
1)当待排序的数组中的元素个数较少时,源码中的阀值为7,采用的是插入排序。尽管插入排序的时间复杂度为0(n^2),但是当数组元素较少时,插入排序优于快速排序,因为这时快速排序的递归操作影响性能。
2)较好的选择了划分元(基准元素)。能够将数组分成大致两个相等的部分,避免出现最坏的情况。例如当数组有序的的情况下,选择第一个元素作为划分元,将使得算法的时间复杂度达到O(n^2).
源码中选择划分元的方法:
当数组大小为 size=7 时 ,取数组中间元素作为划分元。int n=m>>1;(此方法值得借鉴)
当数组大小 7<size<=40时,取首、中、末三个元素中间大小的元素作为划分元。
当数组大小 size>40 时 ,从待排数组中较均匀的选择9个元素,选出一个伪中数做为划分元。
3)根据划分元 v ,形成不变式 v* (<v)* (>v)* v*
普通的快速排序算法,经过一次划分后,将划分元排到素组较中间的位置,左边的元素小于划分元,右边的元素大于划分元,而没有将与划分元相等的元素放在其附近,这一点,在Arrays.sort()中得到了较大的优化。
举例:15、93、15、41、6、15、22、7、15、20
因 7<size<=40,所以在15、6、和20 中选择v = 15 作为划分元。
经过一次换分后: 15、15、7、6、41、20、22、93、15、15. 与划分元相等的元素都移到了素组的两边。
接下来将与划分元相等的元素移到数组中间来,形成:7、6、15、15、15、15、41、20、22、93.
最后递归对两个区间进行排序[7、6]和[41、20、22、93].
package com.util;
public class ArraysPrimitive {
private ArraysPrimitive() {}
/**
* 对指定的 int 型数组按数字升序进行排序。
*/
public static void sort(int[] a) {
sort1(a, 0, a.length);
}
/**
* 对指定 int 型数组的指定范围按数字升序进行排序。
*/
public static void sort(int[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
sort1(a, fromIndex, toIndex - fromIndex);
}
private static void sort1(int x[], int off, int len) {
/*
* 当待排序的数组中的元素个数小于 7 时,采用插入排序 。
*
* 尽管插入排序的时间复杂度为O(n^2),但是当数组元素较少时, 插入排序优于快速排序,因为这时快速排序的递归操作影响性能。
*/
if (len < 7) {
for (int i = off; i < len + off; i++)
for (int j = i; j > off && x[j - 1] > x[j]; j--)
swap(x, j, j - 1);
return;
}
/*
* 当待排序的数组中的元素个数大于 或等于7 时,采用快速排序 。
*
* Choose a partition element, v
* 选取一个划分元,V
*
* 较好的选择了划分元(基准元素)。能够将数组分成大致两个相等的部分,避免出现最坏的情况。例如当数组有序的的情况下,
* 选择第一个元素作为划分元,将使得算法的时间复杂度达到O(n^2).
*/
// 当数组大小为size=7时 ,取数组中间元素作为划分元。
int m = off + (len >> 1);
// 当数组大小 7<size<=40时,取首、中、末 三个元素中间大小的元素作为划分元。
if (len > 7) {
int l = off;
int n = off + len - 1;
/*
* 当数组大小 size>40 时 ,从待排数组中较均匀的选择9个元素,
* 选出一个伪中数做为划分元。
*/
if (len > 40) {
int s = len / 8;
l = med3(x, l, l + s, l + 2 * s);
m = med3(x, m - s, m, m + s);
n = med3(x, n - 2 * s, n - s, n);
}
// 取出中间大小的元素的位置。
m = med3(x, l, m, n); // Mid-size, med of 3
}
//得到划分元V
int v = x[m];
// Establish Invariant: v* (<v)* (>v)* v*
int a = off, b = a, c = off + len - 1, d = c;
while (true) {
while (b <= c && x[b] <= v) {
if (x[b] == v)
swap(x, a++, b);
b++;
}
while (c >= b && x[c] >= v) {
if (x[c] == v)
swap(x, c, d--);
c--;
}
if (b > c)
break;
swap(x, b++, c--);
}
// Swap partition elements back to middle
int s, n = off + len;
s = Math.min(a - off, b - a);
vecswap(x, off, b - s, s);
s = Math.min(d - c, n - d - 1);
vecswap(x, b, n - s, s);
// Recursively sort non-partition-elements
if ((s = b - a) > 1)
sort1(x, off, s);
if ((s = d - c) > 1)
sort1(x, n - s, s);
}
/**
* Swaps x[a] with x[b].
*/
private static void swap(int x[], int a, int b) {
int t = x[a];
x[a] = x[b];
x[b] = t;
}
/**
* Swaps x[a .. (a+n-1)] with x[b .. (b+n-1)].
*/
private static void vecswap(int x[], int a, int b, int n) {
for (int i=0; i<n; i++, a++, b++)
swap(x, a, b);
}
/**
* Returns the index of the median of the three indexed integers.
*/
private static int med3(int x[], int a, int b, int c) {
return (x[a] < x[b] ? (x[b] < x[c] ? b : x[a] < x[c] ? c : a)
: (x[b] > x[c] ? b : x[a] > x[c] ? c : a));
}
/**
* Check that fromIndex and toIndex are in range, and throw an
* appropriate exception if they aren't.
*/
private static void rangeCheck(int arrayLen, int fromIndex, int toIndex) {
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex
+ ") > toIndex(" + toIndex + ")");
if (fromIndex < 0)
throw new ArrayIndexOutOfBoundsException(fromIndex);
if (toIndex > arrayLen)
throw new ArrayIndexOutOfBoundsException(toIndex);
}
}
引用类型采用冒泡排序和归并排序的组合:
对于对象的排序,稳定性很重要。比如成绩单,一开始可能是按人员的学号顺序排好了的,现在让我们用成绩排,那么你应该保证,本来张三在李四前面,即使他们成绩相同,张三不能跑到李四的后面去。
而快速排序是不稳定的,而且最坏情况下的时间复杂度是O(n^2)。
另外,对象数组中保存的只是对象的引用,这样多次移位并不会造成额外的开销,但是,对象数组对比较次数一般比较敏感,有可能对象的比较比单纯数的比较开销大很多。归并排序在这方面比快速排序做得更好,这也是选择它作为对象排序的一个重要原因之一。
排序优化:实现中快排和归并都采用递归方式,而在递归的底层,也就是待排序的数组长度小于7时,直接使用冒泡排序,而不再递归下去。
分析:长度为6的数组冒泡排序总比较次数最多也就1+2+3+4+5+6=21次,最好情况下只有6次比较。而快排或归并涉及到递归调用等的开销,其时间效率在n较小时劣势就凸显了,因此这里采用了冒泡排序,这也是对快速排序极重要的优化