浅谈排序算法
首先介绍一个大佬的算法学习网站:小浩算法
他的这个算法网站在Git星星之多,啧啧,顺便一提我还在他的分群里(笑)~
前段时间按他的分类刷了下leetcode,整个人感觉升华了一般,啊,那个快感啧啧…
以下贴出排序算法的java代码实现,并且介绍一下基本概念。
插入排序:
插入排序就好似整理扑克牌一般,每次的插入都一个个与元素比较,放入合适的位置。
图取自算法导论
左边遍历过排完序的数组就是一个循环不变式。
private static int[] insert_sort_ASC(int[] array){
for (int i = 1; i <array.length ; i++) {
int key=array[i];
int j=i-1;
while (j >= 0&&array[j] > key){
array[j+1]=array[j];
array[j]=key;
j--;
}
}
return array;
}
private static int[] insert_sort_DESC(int[] array){
for (int i = 1; i <array.length ; i++) {
int key=array[i];
int j=i-1;
while (j >= 0&&array[j] < key){
array[j+1]=array[j];
array[j]=key;
j--;
}
}
return array;
}
归并排序:
归并排序使用分而治之的思想,使用递归解决子序列(当其足够小的时候不用递归直接解决),之后再合并(merge)。注:归并排序并不是基于原地址的,故会浪费空间。
private static int[] merge(int[] array1,int[] array2){
int i=0,j=0;
int size=array1.length+array2.length;
int[] merge_array=new int[size];
for (int s = 0; s < size ; s++) {
if (i>=array1.length) {
merge_array[s] = array2[j];
j++;
continue;
}
if (j>=array2.length) {
merge_array[s] = array1[i];
i++;
continue;
}
if (array2[j]<array1[i]){
merge_array[s]=array2[j];
j++;
}else {
merge_array[s]=array1[i];
i++;
}
}
//gc
array1=null;
array2=null;
return merge_array;
}
冒泡排序:
//升序
private static void BubbleSort(int[] array){
for (int i = 0; i <array.length ; i++) {
for (int j = array.length - 1; j > i ; j--) {
if (array[j] < array[j-1]){
array[j]+=array[j-1];
array[j-1]=array[j]-array[j-1];
array[j]-=array[j-1];
}
}
}
}
选择排序:
选择排序和冒泡排序的比较次数相同,但是冒泡排序交换次数多于选择排序。
public int[] sort(int[] sourceArray) {
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// 记录目前能找到的最小值元素的下标
min = j;
}
}
// 将找到的最小值和i位置所在的值进行交换
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
堆排序:
堆排序的时间复杂度同于归并排序,而又和插入排序一样基于原址,结合了两者的优点。
维护堆的过程:
private void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2MRMABBZ-1609218115967)(https://kana-bucket.oss-cn-beijing.aliyuncs.com/%E5%9B%BE%E7%89%87_1598245368623.png)]
建堆(线性时间内将无序数组构造成一个最大堆):
private void buildMaxHeap(int[] arr, int len) {
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
//返回一个最接近的最大整数
heapify(arr, i, len);
//递归建堆
}
}
堆排序算法:
public int[] sort(int[] arr) {
int len = arr.length;
//建堆
buildMaxHeap(arr, len);
for (int i = len - 1; i > 0; i--) {
//每次都剔除堆中最大的元素(堆顶)
swap(arr, 0, i);
len--;
//剔除后重新调整堆,选出下一个堆顶
heapify(arr, 0, len);
}
return arr;
}
快速排序:
在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
private static void quickSort(int[] array,int start,int end){
if (start < end){
int partitionIndex = partition(array, start,end);
quickSort(array,start,partitionIndex-1);
quickSort(array,partitionIndex+1,end);
}
}
private static int partition(int[] array,int start,int end){
int pivot=start;
//设置基准值
int index=pivot+1;
for (int i = index; i <= end ; i++) {
if (array[i]<array[pivot]) {
//小于基准值的 放在左边
swap(array, i, index);
index++;
}
}
swap(array,pivot,index-1);
return index-1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
基数排序:
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数
基数排序:根据键值的每位数字来分配桶;
计数排序:每个桶只存储单一键值;
桶排序:每个桶存储一定范围的数值;
private static void radixSort(int[] arr,int maxDigit){
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++,dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int [][] counter=new int[mod*2][0];
for (int item : arr) {
int bucket = ((item % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], item);
}
int pos=0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
}
/**
* 获取最高位数
*/
private static int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLengh(maxValue);
}
private static int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
private static int getNumLengh(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}
/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private static int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
计数排序:
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
桶排序:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
1.在额外空间充足的情况下,尽量增大桶的数量
2.使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要
private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
if (arr.length == 0) {
return arr;
}
int minValue = arr[0];
int maxValue = arr[0];
for (int value : arr) {
if (value < minValue) {
minValue = value;
} else if (value > maxValue) {
maxValue = value;
}
}
int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
int[][] buckets = new int[bucketCount][0];
// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < arr.length; i++) {
int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
buckets[index] = arrAppend(buckets[index], arr[i]);
}
int arrIndex = 0;
for (int[] bucket : buckets) {
if (bucket.length <= 0) {
continue;
}
// 对每个桶进行排序,这里使用了插入排序
Arrays.sort(bucket);
for (int value : bucket) {
arr[arrIndex++] = value;
}
}
return arr;
}
/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
希尔排序:
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两个方面提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
private void shellSort(int[] arr){
int gap = 1;
while (gap < arr.length/3) {
gap = gap * 3 + 1;
}
while (gap > 0) {
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j = i - gap;
while (j >= 0 && arr[j] > tmp) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
gap = (int) Math.floor(gap / 3);
}
}
二分查找(也可以使用二分来排序):
(可以用于归并排序的划分子序列)
//假设原数组升序
//非递归实现
private static void BinarySearch(int[] array,int target){
if (array==null)
return;
int start = 0;
int end = array.length-1;
while (start <= end){
int mid = start + (start+end) / 2;
if (array[mid]==target) {
System.out.println(mid);
break;
}
if (array[mid]>target){
end=mid-1;
continue;
}
if (array[mid]<target){
start=mid+1;
} }
}
//递归实现
public static int searchRecursion(int target,int[] array) {
if (array != null) {
return searchRecursion(target, 0, array.length - 1,array);
}
return -1;
}
private static int searchRecursion(int target, int start, int end,int[] array) {
if (start > end) {
return -1;
}
int mid = start + (end - start) / 2;
if (array[mid] == target) {
return mid;
} else if (target < array[mid]) {
return searchRecursion(target, start, mid - 1,array);
} else {
return searchRecursion(target, mid + 1, end,array);
}
}
一些经典排序算法的时间复杂度: