文章目录
Java排序算法
冒泡排序|Bubble Sort
冒泡排序是一种简单直观的排序算法
它重复地走访要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来(从左到右从小到大),越小的元素会由不断交换慢慢冒到数列的最前端,所以叫冒泡排序
当输入的数据是正序时最快
当输入的数据是反序时最慢
算法步骤
- 比较相邻的元素,如果第一个大于第二个,交换
- 接着比较第二个和第三个……
- 对每一对相邻元素进行比较,到最后一个
- 可以依次确定最后一个的值,直到排序完成
动画演示
代码实现
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean isSorted = true;
for (int j = 0; j < array.length - i - 1; j++) {
// 相等不交换,保证稳定性
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
isSorted = false;
}
}
if (isSorted) {
break;
}
}
}
private void swap(int[] array, int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
性能分析
- 时间复杂度:
最好:O(n),数据有序
最坏:O(n2),数据逆序
平均:O(n2) - 空间复杂度:O(1)
- 稳定性:稳定
选择排序|Selection Sort
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好
唯一的好处就是不占用额外的内存空间
算法步骤
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
- 重复第二步,直到所有元素均排序完毕
动画演示
代码实现
public static void selectSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
for (int j = i + 1; j < array.length; j++) {
if (array[i] > array[j]) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
性能分析
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定性:不稳定
插入排序|Insert Sort
插入排序是一种最简单直观的排序算法
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
算法步骤
- 将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
动画演示
代码实现
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= 0; j--) {
//如果这里是一个大于等于号 此时这个排序就不稳定了
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
//array[j+1] = tmp;
break;
}
}
array[j + 1] = tmp;
}
}
性能分析
- 时间复杂度:
最好:O(n),数据有序
最坏:O(n2),数据逆序
平均:O(n2)
初始数据越接近有序,时间效率越高 - 空间复杂度:O(1)
- 稳定性:稳定
希尔排序|shell Sort
希尔排序也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序
算法步骤
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1
- 按增量序列个数 k,对序列进行 k 趟排序
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序,仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
动画演示
代码实现
public static void shellSort(int[] array) {
int[] drr = {5, 3, 1};//增量数组
for (int i = 0; i < drr.length; i++) {
shell(array, drr[i]);
}
}
public static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (array[j] > tmp) {
array[j + gap] = array[j];
} else {
break;
}
}
array[j + gap] = tmp;
}
}
性能分析
- 时间复杂度:
最好:O(n),数据有序
最坏:O(n2),数据比较难构造
平均:O(n1.3) - 空间复杂度:O(1)
- 稳定性:不稳定
归并排序|Merge Sort
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写)
- 自下而上的迭代
JavaScript 编译器内存太小,递归太深容易造成内存溢出
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间
算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤 3 直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
动画演示
代码实现
//递归
public static void mergeSort(int[] array) {
mergeSortInternal(array, 0, array.length - 1);
}
public static void mergeSortInternal(int[] array, int low, int high) {
if (low >= high) return;
int mid = (low + high) / 2;
mergeSortInternal(array, low, mid);//左
mergeSortInternal(array, mid + 1, high);//右
//合并
merge(array, low, mid, high);
}
public static void merge(int[] array, int low, int mid, int high) {
/*int s1 = low;
int e1 = mid;
int s2 = mid+1;
int e2 = high;
while(s1<=e1&&s2<=e2){
}*/
int s1 = low;
int s2 = mid + 1;
int[] tmp = new int[high - low + 1];
int k = 0;//tmp数组的下标
while (s1 <= mid && s2 <= high) {
if (array[s1] <= array[s2]) {
/*tmp[k] = array[s1];
k++;
s1++;*/
tmp[k++] = array[s1++];
}
if (array[s1] >= array[s2]) {
tmp[k++] = array[s2++];
}
}
while (s1 <= mid) {
tmp[k++] = array[s1++];
}
while (s2 <= mid) {
tmp[k++] = array[s2++];
}
for (int i = 0; i < tmp.length; i++) {
array[i + low] = tmp[i];
}
}
//非递归
public static void mergeSort2(int[] array) {
for (int i = 1; i < array.length; i *= 2) {
merge2(array, i);
}
}
public static void merge2(int[] array, int gap) {
int s1 = 0;
int e1 = s1 + gap - 1;
int s2 = e1 + 1;
int e2 = s2 + gap - 1 < array.length ? s2 + gap - 1 : array.length - 1;
int[] tmp = new int[array.length];
int k = 0;
//当有两个归并段时
while (s2 < array.length) {
while (s1 <= e1 && s2 <= e2) {//当有两个归并段且这两个归并段都有数据
if (array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
} else {
tmp[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmp[k++] = array[s1++];
}
while (s2 <= e2) {
tmp[k++] = array[s2++];
}
s1 = e2 + 1;
e1 = s1 + gap - 1;
s2 = e1 + 1;
e2 = s2 + gap - 1 < array.length ? s2 + gap - 1 : array.length - 1;
}
while (s1 < array.length) {//退出上边循环后,将s1段内数据拷贝下来,因为有可能e1已经越界
tmp[k++] = array[s1++];
}
for (int i = 0; i < tmp.length; i++) {
array[i] = tmp[i];
}
}
性能分析
- 时间复杂度:O(nlog(n))
- 空间复杂度:O(n)
- 稳定性:稳定
快速排序|Quick Sort
在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见
事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排,但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序
算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot)
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
动画演示
代码实现
//递归
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
public static int pivot(int[] array, int start, int end) {
int tmp = array[start];
while (start < end) {
while (start < end && array[end] >= tmp) {
end--;
}
//把数据赋值给start
array[start] = array[end];
while (start < end && array[start] <= tmp) {
start++;
}
//把start下标的值给end
array[end] = array[start];
}
array[start] = tmp;
return start;
}
public static void quick(int[] array, int low, int high) {
//if(low<high){
if (low >= high) return;
//进行直接插入法优化
if (high - low + 1 <= 50) {
insertSortBound(array, low, high);
return;//一定要return,说明这个区间范围有序了,直接结束
}
//进行三数取中法优化
medianOfThree(array, low, high);
int piv = pivot(array, low, high);
quick(array, low, piv - 1);
quick(array, piv + 1, high);
//}
}
public static void insertSortBound(int[] array, int low, int high) {
for (int i = low + 1; i <= high; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= low; j--) {
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = tmp;
}
}
public static void medianOfThree(int[] array, int low, int high) {
int mid = (low + high) / 2;
if (array[low] <= array[mid]) {
int tmp = array[low];
array[low] = array[mid];
array[mid] = tmp;
}
if (array[low] >= array[high]) {
int tmp = array[low];
array[low] = array[high];
array[high] = tmp;
}
if (array[mid] > array[high]) {
int tmp = array[mid];
array[mid] = array[high];
array[high] = tmp;
}
}
//非递归
public static void quickSort2(int[] array) {
Stack<Integer> stack = new Stack<>();
int low = 0;
int high = array.length - 1;
int piv = pivot(array, low, high);
if (piv > low + 1) {
stack.push(low);
stack.push(piv - 1);
}
if (piv < high - 1) {
stack.push(piv + 1);
stack.push(high);
}
while (!stack.empty()) {
high = stack.pop();
low = stack.pop();
piv = pivot(array, low, high);
if (piv > low + 1) {
stack.push(low);
stack.push(piv - 1);
}
if (piv < high - 1) {
stack.push(piv + 1);
stack.push(high);
}
}
}
性能分析
- 时间复杂度
最好:O(nlog(n))
最坏:O(n2)
平均:O(nlog(n)) - 空间复杂度
最好:O(log(n))
最坏:O(n)
平均:O(log(n)) - 稳定性:不稳定
堆排序|Heap Sort
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
堆排序可以说是一种利用堆的概念来排序的选择排序
分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
动画演示
代码实现
public static void heapSort(int[] array) {
createHeap(array);
for (int i = 0; i < array.length - 1; i++) {
// 交换前
// 无序区间: [0, array.length - i)
// 有序区间: [array.length - i, array.length)
swap(array, 0, array.length - i - 1);
// 交换后
// 无序区间: [0, array.length - i - 1)
// 有序区间: [array.length - i - 1, array.length)
// 无序区间长度: array.length - i - 1
shiftDown(array, array.length - i - 1, 0);
}
}
private void swap(int[] array, int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
private void createHeap(int[] array) {
for (int i = (array.length - 1) / 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
public static void shiftDown(int[] array, int size, int index) {
int left = 2 * index + 1;
while (left < size) {
int max = left;
int right = 2 * index + 2;
if (right < size) {
if (array[right] > array[left]) {
max = right;
}
}
if (array[index] >= array[max]) {
break;
}
int t = array[index];
array[index] = array[max];
array[max] = t;
index = max;
left = 2 * index + 1;
}
}
性能分析
- 时间复杂度:O(nlog(n))
- 空间复杂度:O(1)
- 稳定性:不稳定
部分来自菜鸟教程