好久没有写博客了,就以这篇最近突发奇想的常用排序算法总结做回归吧。
以下是代码
import java.util.concurrent.TimeUnit;
/**
* @Auther: yubotao
* @Description:
* @Date: Created in 19:28 2021/02/21
* @Modified By:
*/
public class SortTest {
private static int[] a = {2,3,5,1,6,2,4,7,5,7,3,5,8,19,33,533,12,453};
public static void main(String[] args) throws Exception {
// TimeUnit.SECONDS.sleep(50);
long start = System.nanoTime();
a = optimizeRadixSort(a);
// radixSort(a);
// recursionMergeSort(a);
// mergeSort(a);
// heapSort(a, true);
// selectSort(a);
// shellSort(a);
// binaryInsertSort(a);
// insertSort(a);
// quickSort(a, 0, a.length - 1);
// bubble(a);
System.out.println("耗时:" + (System.nanoTime() - start) + "ns");
for (int i=0; i<a.length; i++){
System.out.print(a[i] + " ");
}
// TimeUnit.SECONDS.sleep(50);
// System.gc();
// System.out.println("----gc completed");
// TimeUnit.SECONDS.sleep(50);
}
/**冒泡 O(n^2)
* 每趟排序保证第i小的值处于第i个位置
* 第一趟排序将最小值放到第1位,第二趟将第二小放到第2位,依次类推
* **/
public static int[] bubble(int[] a){
int len = a.length;
for (int i=0; i<len; ++i){
for (int j=i+1; j<len; ++j){
if (a[i]>a[j]){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
return a;
}
/**快排 最快O(nlogn)
* 保证每次排序都能将基准值放到它的最终位置上,且每次排序后,它的左侧都小于等于它,右侧都大于等于它
* 具体思想是:获取基准值temp后,取左指针low,右指针high,保证low<high;
* 从队尾开始扫描,如果a[high]<temp,则将high处的值赋给low,即a[low] = a[high];
* 之后从low开始扫描,当a[low]>temp,则将low处的值赋给high,即a[high] = a[low];
* 再从high处开始扫描,如此往复,直到low>=high(low==high),说明找到temp的真正位置,a[low] = temp;
* 接下来以该位置为界,左侧全部小于等于temp,右侧全部大于等于temp,因此各自再进行快排,最后整个数组有序
* 参考文章:https://blog.csdn.net/nrsc272420199/article/details/82587933
* **/
public static void quickSort(int[] a, int low, int high){
if (low<high){
// 寻找基准数据的正确索引
int index = getIndex(a, low, high);
// 对index前后的数组进行相同的操作使整个数组有序
quickSort(a, low, index-1);
quickSort(a, index+1, high);
}
}
static int getIndex(int[] a, int low, int high){
// 基准数据
int temp = a[low];
while (low < high){
// 当队尾元素大于等于基准元素时,向前挪动high指针
while (a[high] >= temp && low<high) {
high--;
}
// 由于队尾元素小于temp,将其值赋给low
a[low] = a[high];
// 当队首元素小于temp,向后挪动low指针
while (a[low] <= temp && low<high) {
low++;
}
// 队首元素大于temp,将其值赋给high
a[high] = a[low];
}
// 此时low和high相等,且是temp元素的最终位置
a[low] = temp;
return low;
}
/**直接插入排序 O(n^2)
* 找出第i个值在已经有序的[1,2,...,i-1]的数组中的插入位置k,将[k,...,i-1]的所有元素后移一位,
* 再将该值插入位置k
* **/
public static void insertSort(int[] a){
int len = a.length;
for (int i=0; i<len; i++){
int item = a[i];
for (int j=0; j<i; j++){
if (item < a[j]){
System.arraycopy(a, j, a, j+1, i-j);
a[j] = item;
break;
}
}
}
}
/**折半插入排序(二分法) O(n^2)
* 在直接插入排序上的改进,通过二分法查找位置
* **/
public static void binaryInsertSort(int[] a){
int len = a.length;
for (int i=1; i<len; i++){
int item = a[i];
int low=0, high=i;
while (low < high){
int middle = (low+high) >>> 1;
if (a[middle] <= item){
low = middle+1;
}else {
high = middle;
}
}
System.arraycopy(a, low, a, low + 1, i - low);
a[low] = item;
}
}
/**希尔排序 O(n^2)
* 简单插入排序的优化,缩小增量排序
* 思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;
* 随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
* 参考文章:https://www.cnblogs.com/chengxiao/p/6104371.html
* **/
public static void shellSort(int[] a){
for (int gap = a.length/2; gap > 0; gap /= 2){
/**
* 精妙之处在于从第gap个元素开始比较,然后逐个向后增加;
* 内部循环会逐渐向左移动,每次移动gap位,直到比较到当前分组最小的值(每一次循环,组内都有序)
* 新分组也就完成了所有比较,变得有序(有冒泡的影子在)
* */
for (int i=gap; i<a.length; i++){
int j = i;
while (j-gap>=0 && a[j]<a[j-gap]){
// 交换元素,通过求和做差避免空间消耗(创建临时变量) 存在溢出风险!
a[j] = a[j] + a[j-gap];
a[j-gap] = a[j] - a[j-gap];
a[j] = a[j] - a[j-gap];
j -= gap;
}
}
}
}
/** 简单选择排序 O(n^2)
* 思路:第i趟排序找到剩下n-i+1个元素中最小的值,进行元素交换,经过n-1趟,排序完成
*/
public static void selectSort(int[] a){
for (int i=0; i<a.length-1; i++){
int k = i;
for (int j=i+1; j<a.length; j++){
if (a[j] < a[k]){
k = j;
}
}
if (k > i) { // 存在溢出风险!
a[i] = a[i] + a[k];
a[k] = a[i] - a[k];
a[i] = a[i] - a[k];
}
}
}
/** 堆排序 O(nlogn)
* 堆一种完全二叉树的顺序存储结构。
* 大根堆(最大的元素在堆顶):满足 L(i) <= L(2i) 且 L(i) <= L(2i+1)
* 小根堆(最小的元素在堆顶):满足 L(i) >= L(2i) 且 L(i) >= L(2i+1)
* 堆排序的关键是构造初始堆,是一个不断调整的过程;插入也是一样;
* 删除堆顶元素时,先将堆的最后一个元素与堆顶元素交换。
* 堆排序的思想:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
* 将其与末尾元素进行交换,此时末尾就为最大值。
* 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
* 如此反复执行,便能得到一个有序序列。
* 参考文章:https://www.cnblogs.com/chengxiao/p/6129630.html
* **/
public static void heapSort(int[] a, boolean increase){
if (increase) bigTopHeapSort(a, a.length);
else smallTopHeapSort(a, a.length);
}
static void bigTopHeapSort(int[] a, int length){ // 大顶堆
if (length == 1) return;
int lastNoLeaf = length/2 - 1; // 完全二叉树,找到最后一个非叶子节点的下标
while (lastNoLeaf >= 0){
if (2*(lastNoLeaf + 1)<length && a[lastNoLeaf] < a[2*(lastNoLeaf + 1)]){ // 右子
swap(a, lastNoLeaf, 2*(lastNoLeaf + 1));
}
if (a[lastNoLeaf] < a[2*lastNoLeaf + 1]){ // 左子
swap(a, lastNoLeaf, 2*lastNoLeaf + 1);
}
lastNoLeaf--;
}
// 此时建立了大顶堆,交换堆顶元素和最后一个元素
swap(a, 0, length-1);
bigTopHeapSort(a, length-1);
}
static void smallTopHeapSort(int[] a, int length){ // 小顶堆
if (length == 1) return;
int lastNoLeaf = length/2 - 1; // 完全二叉树,找到最后一个非叶子节点的下标
while (lastNoLeaf >= 0){
if (2*(lastNoLeaf + 1)<length && a[lastNoLeaf] > a[2*(lastNoLeaf + 1)]){ // 右子
swap(a, lastNoLeaf, 2*(lastNoLeaf + 1));
}
if (a[lastNoLeaf] > a[2*lastNoLeaf + 1]){ // 左子
swap(a, lastNoLeaf, 2*lastNoLeaf + 1);
}
lastNoLeaf--;
}
// 此时建立了小顶堆,交换堆顶元素和最后一个元素
swap(a, 0, length-1);
smallTopHeapSort(a, length-1);
}
static void swap(int[] a, int left, int right){ // 异或操作可以避免溢出
a[left] = a[left] ^ a[right];
a[right] = a[left] ^ a[right];
a[left] = a[left] ^ a[right];
}
/**归并排序 O(nlogn)
* 分治法的应用 这里只讨论朴素的2路归并,目前最优的归并算法时TimSort,这个刚好之前读过paper和源码
* 思想:通过将待排序序列分解成n个容量为2的单位,分别排序,再将所有单位合并再排序
* 参考文章:https://www.cnblogs.com/chengxiao/p/6194356.html
* TimSort算法会整理一篇博客,到时候把地址贴过来
* **/
public static void mergeSort(int a[]){ // 自实现,需要优化的点挺多
int capacity = 2; // 排序的容量单位
// 分治
for (int i=0; i<a.length; i += capacity){ // 暂时只考虑2路归并,如果修改容量,则要修改此处代码
if (i+1<a.length && a[i] > a[i+1]){
swap(a, i, i+1);
}
}
// 合并
while (capacity <= a.length){
int k = 0; // 指针,防止越界
while (k < a.length){
int left = k; // 左侧待合并序列第一个元素指针,容量为capacity
int right = k+capacity; // 右侧待合并序列第一个元素指针
int[] temp = new int[2*capacity]; // 临时数组 频繁开辟数组
int tempSize = 0; // 临时数组的真实容量
while (tempSize < 2*capacity){
if (left < a.length && right < a.length){
if (left < k+capacity && right < k+2*capacity) {
if (a[left] <= a[right]) {
temp[tempSize++] = a[left++];
} else {
temp[tempSize++] = a[right++];
}
}else{
while (left < k+capacity){ // 左侧集合还有剩余
temp[tempSize++] = a[left++];
}
while (right < k+2*capacity){ // 右侧集合还有剩余
temp[tempSize++] = a[right++];
}
}
}else {
break;
}
}
// 将排好的临时数组复制回原始数组
System.arraycopy(temp, 0 , a, k, tempSize);
k = k+2*capacity;
}
capacity *= 2; // 扩容
}
}
// 递归实现
public static void recursionMergeSort(int[] a){
int[] temp = new int[a.length]; // 提前新建临时数组,避免频繁开辟空间
sort(a, 0, a.length-1, temp);
}
static void sort(int[] a, int left, int right, int[] temp){
if (left<right){
int mid = (left+right)/2;
sort(a, left, mid, temp); // 左侧归并排序
sort(a, mid+1, right, temp); // 右侧归并
merge(a, left, mid, right, temp); // 合并
}
}
static void merge(int[] a, int left, int mid, int right, int[] temp){
int i=left; // 左侧集合指针
int j=mid+1; // 右侧指针
int t=0; // 临时数组指针
while (i<=mid && j<=right){
if (a[i] <= a[j]){
temp[t++] = a[i++];
}else {
temp[t++] = a[j++];
}
}
while (i<=mid){
temp[t++] = a[i++];
}
while (j<=right){
temp[t++] = a[j++];
}
// 将临时数组的值复制回原始数组
t=0;
while (left<=right){
a[left++] = temp[t++];
}
}
// 注:通过jconsole的监测得出结果,提前创建最大容量临时数组所占用的空间反而比频繁新建还要多(因为运行中会被gc)
// 如何测试:https://blog.csdn.net/weixin_44663675/article/details/107089808
/**基数排序法 O(nlog(r)m) 其中r为所采取的基数,m为堆数
* 属于分配式排序,通过键值的部分信息,将要排序的元素分配至某些“桶”中,以达到排序的作用
* 分为最高位优先法——MSD和最低位优先法——LSD
* 排序思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的前面补零。
* 然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
* 参考文章:https://blog.csdn.net/lemon_tree12138/article/details/51695211
* **/
public static void radixSort(int[] a){
int[][] temp = new int[10][a.length]; // 临时数组
int[] order = new int[10]; // 属于该位的个数
int digit = 1;
int maxLength = getMaxLength(a);
while (maxLength >= digit){
for (int i=0; i<a.length; i++){
int digitNum = getDigit(a[i], digit);
temp[digitNum][order[digitNum]] = a[i];
order[digitNum]++;
}
int aPoint = 0;
for (int i=0; i<10; i++){
for (int j=0; j<order[i]; j++){
a[aPoint++] = temp[i][j];
temp[i][j] = 0;
}
order[i] = 0;
}
digit++;
}
}
static int getDigit(int num, int digit){
int result = 0;
while (digit>0){
result = num%10;
num /= 10;
digit--;
}
return result;
}
static int getMaxLength(int[] a){
int maxLength = 0;
for (int i : a){
int length = String.valueOf(i).length();
maxLength = maxLength>length ? maxLength : length;
}
return maxLength;
}
/** 空间优化
* 思路:因为创建二维临时数组存在空间浪费,空间利用率过低,因此通过如下方式提高空间利用率
* 首先我们使用一个和原数组长度相同的临时数组bucket[]来暂存数据,同时通过另一个计数数组count[10]来标识位置
* 该计数数组长度为10(以10为基底的基数排序),它每位保存的值为前一位保存的个数+当前位的个数
* 举例说明:假设数组[2314, 5428, 373, 2222, 17],则计数数组count[10]第一次排序以最低位
* count[10] 0 1 2 3 4 5 6 7 8 9
* 0 0 1 2 3 3 3 4 5 5
* 上述含义为,最低位为2的有1个数,最低位为3的也是一个数(2 = 1+1),同理最低位为8的是一个数(5 = 4+1)
* 第二次排序,以倒数第二位排序得
* count[10] 0 1 2 3 4 5 6 7 8 9
* 0 2 4 4 4 4 4 5 5 5
* 为什么这么做呢?因为这样临时数组bucket[]的下标就可以直接通过count[]得到。
* 比如当我们找到某个数的第i位的值时,以373为例,在第二次循环,我们得到的值为7
* 同时7也是该位最大的值,应该放到数组的末尾,此时我们观察count[7],发现直接取count[7]--
* 刚好就能得到373在bucket[]中的位置bucket[4]。
* 这样我们通过以上方式做到了空间压缩。
* @param array
* @return
*/
public static int[] optimizeRadixSort(int[] array) {
int maxLength = getMaxLength(array);
return sortCore(array, 1, maxLength);
}
private static int[] sortCore(int[] array, int digit, int maxLength) {
if (digit > maxLength) {
return array;
}
int radix = 10; // 基数
int arrayLength = array.length;
int[] count = new int[radix];
int[] bucket = new int[arrayLength];
// 统计将数组中的数字分配到桶中后,各个桶中的数字个数
for (int i = 0; i < arrayLength; i++) {
count[getDigit(array[i], digit)]++;
}
// 将各个桶中的数字个数,转化成各个桶中最后一个数字的下标索引
for (int i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
// 将原数组中的数字分配给辅助数组 bucket
for (int i = arrayLength - 1; i >= 0; i--) {
int number = array[i];
int d = getDigit(number, digit);
bucket[count[d] - 1] = number;
count[d]--;
}
return sortCore(bucket, digit + 1, maxLength);
}
}