基本排序算法实现与分析
冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序代码实现与特性分析
冒泡排序
实现代码
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
//从0号开始比较,每次把最大的搬运到末尾,arr[i]为每次循环确定的位于最终位置的那个元素
bool flag = true; //flag用来作为标记
for (int i = arr.length - 1; i > 0 && flag; i--) {
flag = false;
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
flag = true;
}
}
}
}
理论分析
排序过程中局部有序。
在应用了哨兵后初始序列有序情况下效率最高,为O(n)
最坏、平均时间复杂度为O(n^2),最好时间复杂度为O(n)
空间复杂度为O(1)
插入排序
实现代码
//每次循环使得[0,i]成为有序序列,元素i找到从后向前正确的插入位置
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
理论分析
时间复杂度与数据初始状况相关,最好情况下也就是初始有序的情况下,时间复杂度为O(n),逆序为最差情况,为O(n2)。平均时间复杂度O(n2)。
空间复杂度为O(1)
稳定
选择排序
实现代码
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
//每轮循环找i到arr.length - 1中最小的,和i位置上的数交换
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
理论分析
最好、最差、平均时间复杂度均为O(n^2)
空间复杂度为O(1)
不稳定
时间效率与初始序列无关
归并排序
实现代码
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);//T(N/2)
mergeSort(arr, mid + 1, r);//T(N/2)
merge(arr, l, mid, r);//O(N)
//T(N) = 2*T(2/N) + O(N)
}
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
理论分析
根据master算法,T(N) = 2*T(2/N) + O(N) ,即a=2,b=2,d=1
所以,时间复杂度为O(NlogN)
归并排序的最好、最差、平均时间复杂度均为O(NlogN)
归并排序的空间复杂度为O(N)
算法效率与初始序列状态无关
快速排序
实现代码
public class Sort {
//经典快排思路,划分时选右边界做标准,然后从左向右挤
public static int partition(int[] arr,int l,int r){
int less = l - 1;
for(int i = l;i <= r;i++){
if(arr[i] < arr[r]){
swap(arr,++less,i);
}
}
swap(arr,less + 1,r);
return less + 1;
}
public static void quickSort(int[] arr,int l,int r){
if(l >= r)
return;
int p = partition(arr,l,r);
quickSort(arr,l,p - 1);
quickSort(arr,p + 1,r);
}
public static void swap(int[] arr,int i,int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
理论分析
最好、平均时间复杂度为O(nlogn),最坏时间复杂度为O(n^2)
空间复杂度为O(1)
时间效率与初始序列有关,初始有序时效率最差为O(n^2)
不稳定
堆排序
代码实现
//排序,因为每次堆顶元素都被放在了存储堆的数组后的第一个位置,所以排序后为增序,大根堆
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
//新的元素入堆,从下往上调整
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
//当顶部元素出堆,堆内最后一个元素补上堆顶元素的位置,再对该元素进行调整,从上往下调整
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
理论分析
时间复杂度O(NlogN),额外空间复杂度O(1)
建立堆的时间复杂度为O(N) (应为log1+log2+…+logn收敛于n )
大根堆:在表示堆的完全二叉树中,任何子树的最大值都是子树的根节点
小根堆:在表示堆的完全二叉树中,任何子树的最小值都是子树的根节点
二叉树左子节点下标2*i+1,右子节点下标2*i+2
父节点下标(i- 1)/2
堆一定是完全二叉树,并且是用数组进行存储,使用下标关系进行子父节点对应
java排序函数逻辑
- 长度小于60用插入排序,因为常数项极少,小样本下速度快
- 而大样本时使用快排/归并先划分,当样本数变少后再用插入排序。类似于原来是if(L==R) return;现在是LR之间有60个元素,再返回
- 基本数据类型(int,float,double...)排序用快排(因为基本数据类型只要相等就一样,没必要保证稳定性)
- 对象排序用归并(可以保证稳定性)
注:一个数组,要求把奇数放左边,偶数放右边,并保持奇数偶数部分内部顺序不变,空间复杂度O(1)时间复杂度O(n)——无法做到,因为要是能做到快排的划分操作就可以保证稳定性,这两个本质上都是0-1操作划分
总结
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 效率是否与初始序列有关 |
---|---|---|---|---|
冒泡排序 | 最坏、平均为O(n^2),最好为O(n) | O(1) | 稳定 | 相关(哨兵机制,如不使用哨兵则无关),初始有序时最好时间复杂度为O(n) |
归并排序 | 最好、最差、平均均为O(NlogN) | O(n) | 稳定 | 无关 |
插入排序 | 最好O(n),最差、平均为O(n^2) | O(1) | 稳定 | 相关,初始有序时最好时间复杂度为O(n),逆序时最差为O(n^2) |
选择排序 | 最好、最差、平均均为O(n^2) | O(1) | 不稳定 | 无关 |
快速排序 | 最好、平均为O(nlogn),最坏为O(n^2) | O(1) | 不稳定 | 相关,初始有序时效率最差为O(n^2),解决方案为随机选择划分元素 |
堆排序 | O(NlogN) | O(1) | 不稳定 | 堆排这个就不说相不相关了吧 |
基数排序 | O(d*(r+n)),d为排序码的位数,r为排序码基数(10)d小n大时适合 | O(1) | 稳定 |