排序算法 | 平均时间复杂度 |
---|---|
冒泡排序 | O(n2) |
选择排序 | O(n2) |
插入排序 | O(n2) |
希尔排序 | O(n1.5) |
快速排序 | O(N*logN) |
归并排序 | O(N*logN) |
堆排序 | O(N*logN) |
基数排序 | O(d(n+r)) |
8大排序算法分析和总结
代码如下
/**
* <h1>冒泡排序</h1>
* <b>基本思想:两个数比较大小,较大的数下沉,较小的数冒起来。
* 大小为n的数组,从后向前相邻的数两两比较,最小数不断被交换提前,最小数被交换到起始的位置,
* 每次循环比较完起始位置+1,直到起始位置为n-1。
*/
public static void bubbleSort(int[] a) {
int l = a.length;
for (int i = 0; i < l - 1; i++) { // 表示趟数,一共arr.length-1次。
for (int j = l - 1; j > i; j--) {
if (a[j] < a[j - 1]) {
a[j] ^= a[j - 1];
a[j - 1] ^= a[j];
a[j] ^= a[j - 1];
}
}
}
}
// 冒泡排序的2种思想
// 冒泡 从下往上冒泡
public static void bubbleSort(int[] a) {
int l = a.length;
for (int i = 0; i < l - 1; i++) { // 表示趟数,一共arr.length-1次。
for (int j = l - 1; j > i; j--) {
if (a[j] < a[j - 1]) {
a[j] ^= a[j - 1];
a[j - 1] ^= a[j];
a[j] ^= a[j - 1];
}
}
}
}
// 冒泡 从上往下沉
public static void bubbleSort2(int[] a) {
int l = a.length;
for (int i = 0; i < l- 1; i++) {
// 从上往下沉
for (int j = 0; j < l - 1 - i; j++) {
if (a[j] > a[j + 1]) {
a[j] ^= a[j + 1];
a[j + 1] ^= a[j];
a[j] ^= a[j + 1];
}
}
}
}
/**
* <h1>选择排序</h1>
* <b>基本思想: 在长度为N的无序数组中,
第1次遍历n-1个数,找到最小的数值与第1个元素交换;
第2次遍历n-2个数,找到最小的数值与第2个元素交换;
...
第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
*/
public static void selectSort(int[] a) {
for (int i = 0; i < a.length - 1; i++) { // 表示趟数,一共arr.length-1次。
int min = i;
for (int j = i + 1; j > a.length; j++) {
if (a[j] < a[min]) {
min = j;
}
}
if (min != i) {
a[i] ^= a[min];
a[min] ^= a[i];
a[i] ^= a[min];
}
}
}
// 选择排序排序优化,将最大值和最小值对称排序
public static void selectSort2(int[] arr) {
int temp;
for (int i = 0; i < arr.length / 2; i++) {
int minIndex = i;
int maxIndex = arr.length - 1 - i;
for (int j = i + 1; j < arr.length - i; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
if (arr[j] > arr[maxIndex]) {
maxIndex = j;
}
}
if (minIndex != i) {
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
if (maxIndex != (arr.length - 1 - i)) {
temp = arr[(arr.length - 1 - i)];
arr[(arr.length - 1 - i)] = arr[maxIndex];
arr[maxIndex] = temp;
}
}
}
/**
* <h3>插入排序</h3>
* <b>基本思想: 在要排序的一组数中,
假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,
使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
*/
public static void insertSort(int[] arr) {
int temp;
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i + 1; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
} else {
break;
}
}
}
}
/**
* 希尔排序
* <b>基本思想:使用希尔增量+插入排序
*/
public static void shellSort(int[] arr) {
for (int gap = a.length / 2; gap > 0; gap /= 2) {
for (int i = 0; i < a.length - gap; i ++) {
for (int j = i + gap; j >= gap; j -= gap) {
if (a[j] < a[j - gap]) {
exc(a, j, j - gap);
} else {
break;
}
}
}
}
}
/**
* <h3>快速排序</h3>
* <b>基本思想:先从数列中取出一个数作为key值;
将比这个数小的数全部放在它的左边,
大于或等于它的数全部放在它的右边;
对左右两个小数列重复第二步,直至各区间只有1个数。
* 挖坑填数+分治
*/
public static void quickSort(int[] data, int start, int end) {
if (data == null || start >= end)
return;
int i = start, j = end;
int pivotKey = data[start];
while (i < j) {
while (i < j && data[j] >= pivotKey)// 从右向左找第一个小于key的值
j--;
// 挖坑填数
if (i < j)
data[i++] = data[j];
while (i < j && data[i] <= pivotKey)// 从左向右找第一个大于key的值
i++;
// 挖坑填数
if (i < j)
data[j--] = data[i];
}
// 循环退出时,i等于j,将第首次的中位数填到这个坑中。
data[i] = pivotKey;
// 分治
quickSort(data, start, i - 1);
quickSort(data, i + 1, end);
}
/**
* 快速排序 方式2
* 关键点:给定一组数列,随机抽取一个数pivot作为中间数,要求中间数左边的数小于中间数,中间数右边的数大于中间数
*
* @param arr
* @param left
* @param right
*/
public static void quickSort2(int[] arr, int left, int right) {
if (left >= right) return;
int l = left;
int r = right;
int pivot = arr[right];
while (l < r) {
while (arr[l] < pivot) l++;// 从右找到大于pivot 的 索引值,没有就 ++
while (arr[r] > pivot) r--;// 从左找到小于pivot 的 索引值,没有就 ++
if (l < r) {// 当进行最后一次循环时,l > r,防止交换
// 交换
if (arr[l] != arr[r]) { // 值相等时,异或的值为0,交换出错
arr[l] ^= arr[r];
arr[r] ^= arr[l];
arr[l] ^= arr[r];
}
//System.out.printf("%s %d %d %d\n", Arrays.toString(arr), l, r, pivot);
if (arr[l] == pivot && arr[r] == pivot) { // 当pivot重复时候,防止无限循环
l++;
}
}
}
quickSort(arr, left, l - 1);
quickSort(arr, l + 1, right);
//System.out.printf("l=%d,r=%d \n", l, r);
}
/**
* <h3>归并排序</h3>
* <b>基本思想:首先考虑下如何将2个有序数列合并。
这个非常简单,只要从比较2个数列的第一个数,谁小就先取谁,
取了后就在对应数列中删除这个数。然后再进行比较,如果有数
列为空,那直接将另一个数列的数据依次取出即可。
*
* 采用分治法的一个非常典型的应用
*/
public static void mergeSort(int[] data, int start, int end) {
if(start >= end)return;
int mid = (start + end)/2;
mergeSort(data,start,mid);
mergeSort(data,mid+1,end);
// 分治法思想
mergeArray(data,start,mid,end);
}
public static void mergeArray(int[] data, int start, int mid, int end) {
// 声明临时数组用于赋值
int[] temp = new int[(end-start+1)];
int i = start, m = mid;
int j = mid + 1, n = end;
int k = 0;
while (i <= m && j <= n) {
if (data[i] <= data[j]) {
temp[k++] = data[i++];
} else {
temp[k++] = data[j++];
}
}
while (i <= m)
temp[k++] = data[i++];
while (j <= n)
temp[k++] = data[j++];
// 最后将临时数组的值复制到给定数组的指定位置
for (int s = 0; s < k; s++) {
data[start + s] = temp[s];
}
}
/**
* <h3堆排序</h3>
* <b>基本思想:
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
在一个堆中,位置i的结点的父元素的位置是(i+1)/2-1,而它的两个子节点的位置分别是2i+1和2i+2,
这样我们就可以通过计算数组的索引在树中上下移动。
那么我们 进行堆排序, 应该怎么做呢?首先,我们得构建一个堆(大顶堆)。
构建的思路就是:我们将小的元素下沉(sink())即可。
*/
public static void heapSort(int[] arr) {
int n = arr.length;
// 1.构建大顶堆
// 位置n-1的结点的父元素的位置是(n-1 +1)/2-1
for (int i = (n - 1) / 2; i >= 0; i--) {
sink(arr, i, n - 1);
}
// 2.进行堆排序
/* 将对顶的元素移动到最后使得末尾的元素最大,
* 然后我们继续调用sink函数,又可以使得堆顶的元素最大(实则为总的第二大),
* 然后继续重复以前的操作即可。*/
for (int i = n - 1; i >= 1; i--) {
// 把堆顶元素与最后一个元素交换
exc(arr, 0, i);
// 把打乱的堆进行调整,恢复堆的特性
sink(arr, 0, i - 1);
}
}
// 下沉操作,n为给定的最后截止索引
public static void sink(int[] arr, int parent, int n) {
// 临时保存要下沉的元素,用于挖坑填数
//int temp = arr[parent];
// 定位左孩子节点的位置2i+1,开始下沉
int child;
while ((child = 2 * parent + 1) <= n) {
// 如果右孩子节点比左孩子大,则定位到右孩子
if (child + 1 <= n && arr[child] < arr[child + 1])
child++;
// 如果孩子节点小于或等于父节点,则下沉结束
// if (arr[child] <= temp)
if (arr[child] <= arr[parent])
break;
// 父节点进行下沉,挖坑填数
// arr[parent] = arr[child];
exc(arr,child,parent);
// 子节点变成父节点
parent = child;
}
// arr[parent] = temp;
}
// 交换数组2个索引的值
public static void exc(int a[],int i,int j){
// 当他们相等的时候就没必要进行交换
if(a[i] != a[j]){
a[i] ^= a[j];
a[j] ^= a[i];
a[i] ^= a[j];
}
}
/**
* <h3基数排序</h3>
* <b>基本思想:
先以个位数的大小来对数据进行排序,接着以十位数的大小来多数进行排序,
接着以百位数的大小……排到最后,就是一组有序的元素了。不过,他在以某位数进行排序的时候,
是用“桶”来排序的。由于某位数(个位/十位….,不是一整个数)的大小范围为0-9,
所以我们需要10个桶,然后把具有相同数值的数放进同一个桶里,
之后再把桶里的数按照0号桶到9号桶的顺序取出来,
这样一趟下来,按照某位数的排序就完成了
*/
public static void radixSort(int[] a) {
// temp:临时数组
// n:序列的数字个数
// k:最大的位数2
// r:基数10
// cnt:存储bin[i]的个数
int n = a.length;
int temp[] = new int[n];
int r = 10;
int k = 0;
// 获取最大位数
for (int i = 0; i < n; i++) {
int length = (a[i] + "").length();
if (length > k)
k = length;
}
for (int i = 0, rtok = 1; i < k; i++, rtok = rtok * r) {
// 初始化10个箱子
int[] cnt = new int[r];
// 计算每个箱子记录的数字个数
// 计算a[j]在箱子的位置,并在该位置上+1
for (int j = 0; j < n; j++) {
cnt[(a[j] / rtok) % r]++;
}
// cnt[j]的个数修改为记录前j个箱子共有几个数字
// 目的:计算a[j]在temp的临时数组位置
for (int j = 1; j < r; j++) {
cnt[j] += cnt[j - 1];
}
/* 重点理解:cnt[j]保存了前j个箱子的数字个数和,
* 所以从a[n-1]开始计算,定位箱子得值减1即可获取保存在temp的位置
*/
for (int j = n - 1; j >= 0; j--) {
// 计算a[j]在temp的临时数组位置
cnt[(a[j] / rtok) % r]--;
temp[cnt[(a[j] / rtok) % r]] = a[j];
}
for (int j = 0; j < n; j++) {
a[j] = temp[j];
}
}
}
public static void main(String[] args) {
int[] arr = new Random().ints(10, 0, 100).toArray();
System.out.println(Arrays.toString(arr));
quickSort(arr,0,arr.length -1);
System.out.println(Arrays.toString(arr));
// 不使用其他变量,交换a,b的值异或方式
/*int a = 14;
int b = 5;
System.out.println(a+","+b);
a=a^b;
b=a^b;
a=a^b;
System.out.println(a+","+b);
*/
}
**有一个交换位置的函数exc,不使用临时变量,而是使用异或^=
/**
* 交换a数组中i和j的位置
* @param a 需要交换的数组
* @param i 位置
* @param j 位置
*/
public static void exc(int a[],int i,int j){
// 当他们相等的时候就没必要进行交换
if(a[i] != a[j]){
a[i] ^= a[j];
a[j] ^= a[i];
a[i] ^= a[j];
}
}
---对算法优化的理解:要想写出最优的程序就需要理解计算机运行程序的过程,代码的每1行都会加载到内存再到CPU运算(包括寻址取值、逻辑计算),那么从减少变量声明和尽可能的复用、减少循环体代码、优化循环判断逻辑和循环运算逻辑、递归的尾调用优化,等等都需要考虑在内,以“挖坑填数”为例,使用循环体外的变量复用使代码运行的最佳。