• 排序算法(java版)


    一直想理解一下基本的排序算法,最近正好在搞java所以就一并了(为了便于理解,这儿都是以从小到大排序为目的)

    冒泡排序

     也就是比较连续的两个值,如果前面一个值大于后面一个值,则交换。

    时间复杂度为O(n*n),是稳定排序(稳定性意思:当两个值相同时,排序前后的这两个值的相对位置是否有交换)

    注意点:第二重循环参数

        // 冒泡排序,从小到大
        private static void BubbleSort(int[] arr, int n) {
            for (int i = 0; i < n; ++i) {
                for (int j = 0; j < n - i - 1; ++j) {
                    if (arr[j] > arr[j + 1]) 
                        Swap(arr, j, j + 1);
                }
            }
        }

    选择排序

    就是从头到尾选择每个位置放这个位置到最后一个位置的最小值,其中我们可以标记,减少交换次数

    时间复杂度为O(n*n),不稳定的排序

    注意点:判断后再进行异或的交换

        // 选择排序
        private static void SelectSort(int[] arr, int n) {
            int temp;
            for (int i = 0; i < n; ++i) {
                temp = i;
                for (int j = i + 1; j < n; ++j) {
                    if (arr[temp] > arr[j]) {
                        temp = j;
                    }
                }
                // 异或交换时必须判断
                if (i != temp)
                    Swap(arr, i, temp);
            }
        }

    直接插入排序

    就是从头到尾遍历每个点,每个点再从尾到头找这个值放在数组的位置

    时间复杂度为O(n*n),稳定的排序

    注意点:temp与谁比较

        // 直接插入排序
        private static void InsertSort(int[] arr, int n) {
            // 哨兵
            int temp;
            for (int i = 1; i < n; ++i) {
                temp = arr[i];
                int k = i - 1;
                // 注意从后向前
                for (int j = k; j >= 0 && temp < arr[j]; --j, --k) {
                    arr[j + 1] = arr[j];
                }
                arr[k + 1] = temp;
            }
        }

    快速排序

    其实就类似分治的方法,取一个中枢元素,然后找到一个位置放这个中枢元素,保证这个位置前面的值不大于它,后面的值不小于它,接着分别递归

     最坏时间复杂度为O(n*n),平均时间复杂度为O(n*log2n),所以可以先打乱顺序。是不稳定的排序

    注意点:中间两个while的顺序

        // 模拟洗牌打乱顺序
        private static void Shuffle(int[] arr, int n) {
            Random random = new Random();
            for (int i = 0; i < n; ++i) {
                int x = random.nextInt(n - 1);
                int y = random.nextInt(n - 1);
                if (x != y)
                    Swap(arr, x, y);
            }
            Print(arr, n);
        }
        // 快速排序(最坏时间是n*n,所以我们首先可以打乱顺序)
        private static void quickSort(int[] arr, int left, int right) {
            // 哨兵
            int temp = arr[left];
            int i = left, j = right;
    
            // 出口
            if (i >= j)
                return;
    
            // 保证左边一定不比temp大,右边一定不比temp小
            while (i < j) {
                while (i < j && arr[j] >= temp) {
                    --j;
                }
                if (i < j) {
                    arr[i] = arr[j];
                    ++i;
                }
                while (i < j && arr[i] <= temp) {
                    ++i;
                }
                if (i < j) {
                    arr[j] = arr[i];
                    --j;
                }
            }
            arr[i] = temp;
    
            // 左右递归
            quickSort(arr, left, i - 1);
            quickSort(arr, i + 1, right);
        }

    二路归并排序

    其实很简单,就是从中间分成左右两个数组分别递归,在最后返回时我们保证两个数组都是有序数组再合并,可以知道使用双指针的方法可以在O(n)的

    时间内合并两个有序数组,然后知道这样分下去其实是几乎完全的二叉树,高度为log2n

     时间复杂度为O(n*log2n),是稳定的排序

    注意点:指针向后移动

        // 二路归并排序
        private static void Marge(int[] arr, int left, int mid, int right) {
            int[] temp = new int[right - left + 1];
            int leftpos = left, rightpos = mid + 1;
            // 双指针方法
            for (int i = 0; i < right - left + 1; ++i) {
                if (rightpos > right || leftpos <= mid && arr[leftpos] <= arr[rightpos]) {
                    temp[i] = arr[leftpos++];
                } else {
                    temp[i] = arr[rightpos++];
                }
            }
            for (int i = 0; i < right - left + 1; ++i) {
                arr[i + left] = temp[i];
            }
        }
        private static void MargeSort(int[] arr, int left, int right) {
            if (left >= right)
                return;
            // 分成两段递归
            int mid = (left + right >> 1);
            MargeSort(arr, left, mid);
            MargeSort(arr, mid + 1, right);
            // 关键:合并两端有序数组,只需用O(n)时间做到
            Marge(arr, left, mid, right);
        }

    希尔排序

    极其简单的算法,简答的方法是是设置步长并每次减一半,接着使用插入排序,排的是相隔步长的元素

     时间复杂度很麻烦,但是最坏也是O(n*n),平均大概是O(n^3/2),是不稳定排序

        // 希尔排序
        private static void HillSort(int[] arr, int n) {
            // 设置步长,即保证相隔步长个元素为有序数组
            for (int step = n / 2; step > 0; step /= 2) 
                // 修改的插入排序
                for (int i = step; i < n; ++i) 
                    for (int j = i; j - step >= 0; j -= step) 
                        if (arr[j - step] > arr[j]) 
                            Swap(arr, j - step, j);
        }

    堆排序

    为了让额外的空间复杂度为O(1),可以首先倒着使用原数组下沉的思想(就是保证了此点的子树一定是堆)建立大顶堆,

    接着依次将头放到新空出来的位置,再更新堆

    时间复杂度是O(n*log2n),是不稳定排序

    注意点:边界值得大小

        // 堆的下沉,保证此树为堆
        private static void DeleteEmp(int[] arr, int fat, int n) {
            int lefchild;
            int temp = arr[fat];
            for (; (fat << 1 | 1) < n; fat = lefchild) {
                lefchild = fat << 1 | 1;
                if (lefchild < n - 1 && arr[lefchild] < arr[lefchild + 1])
                    lefchild++;
                if (arr[lefchild] > temp) {
                    arr[fat] = arr[lefchild];
                } else {
                    break;
                }
            }
            arr[fat] = temp;
        }
        // 堆排序(额外的辅助空间为O(1)),聪明的方法是利用原来的空间直接建树,
        // 但是从小到大时我们需要建立大顶堆,接着删除第一个点放到数组最后一个位置,倒着插入
        private static void HeapSort(int[] arr, int n) {
            // 建堆(注意普通建堆时间复杂度为O(n),因为上浮的平均时间复杂度为O(1))
            // 保证以个点为树的都是堆,倒着来就可以保证整棵树
            for (int i = n / 2; i >= 0; --i) {
                DeleteEmp(arr, i, n);
            }
            // 堆顶放入后面,再调整堆
            for (int i = n - 1; i > 0; --i) {
                Swap(arr, 0, i);
                DeleteEmp(arr, 0, i);
            }
        }

    以上都是基于比较的排序方法,接下来介绍一些非基于比较的排序算法,而且都是线性的时间复杂度

    计数排序

    就是将数字变成数组下标,这种排序主要是保证数据范围比较小。

    时间复杂度为O(k),是不稳定的排序

    基数排序

    首先从低到高枚举的是数字的每一位,每一位根据0-9的顺序放入桶中储存,再放回原数组,注意负数,还有就是非常耗费空间

    时间复杂度为O(n),是稳定的排序

        // 基数排序,时间复杂度为O(n)的排序方式,m为最大数字的位数+1
        private static void BaseSort(int[] arr, int n, int m) {
            // 假设数字范围为(0,1000000000)
            int[] order = new int[10];
            int[][] temp = new int[10][n];
            int mm = 0, k = 1;
            while (mm < m) {
                // 从最低位开始放入10个桶中
                for (int i = 0; i < n; ++i) {
                    int last = arr[i] / k % 10;
                    temp[last][order[last]++] = arr[i];
                }
                // 10个桶中按顺序放回数组
                int coun = 0;
                for (int i = 0; i < 10; ++i) {
                    if (order[i] > 0) {
                        for (int j = 0; j < order[i]; ++j)
                            arr[coun++] = temp[i][j];
                        order[i] = 0;
                    }
                }
                k *= 10;
                ++mm;
            }
        }

    桶排序

    将数组分到有限数量的桶子里。每个桶子再个别排序

    时间复杂度为O(n),是稳定的排序

    接着还有一些比较奇特的排序方式:

    鸡尾酒排序:双向冒泡排序

    梳排序:在冒泡排序下让步长不为1,而且每次步长除以1.3

    奇偶排序:多核处理有优势;先将待排序数组的所有奇数位与自己身后相邻的偶数位相比较,如果前者大于后者,则进行交换,直到这一趟结束。然后将偶数位与自己身后相邻的奇数位相比较,如果前者大于后者,则进行交换,直到这一趟结束。重复

    外排序:以上都是在内存中排序的算法,即可以在内存中直接寻址,而外排序则可能是输入数据在磁带上,它使用的是归并排序,但是可以是多路归并排序

    树形选择排序(锦标赛排序) :对N个数字,选出最小(大)的n/2个数,再进行新一轮的比较,直到选出最小(大)的。

    1.把N个数放到完全二叉树的叶子节点,两两比较,选出最小的作为根节点,且保存到数组中

    2.把最小的原始值设为无穷大,从那个地方开始新一轮比较

    第一次需要比较n-1次,之后都是log2(n)次

    小结

    对于一般的内部排序,基本使用的是插入排序,希尔排序或者快速排序,主要是根据输入数据的大小来确定。

    注意希尔排序可以使用Sedgewick增量运行,则预计运行时间就为O(N^7/6),而快速排序找枢纽可以使用三数中值分割法。

    堆排序比希尔排序要慢,但是也可以根据Floyd提出的改进算法移动一次数据来优化。

  • 相关阅读:
    PKUSC 2018 题解
    [bzoj 1758] 重建计划
    bzoj 5329 [SDOI2018] 战略游戏
    bzoj 5285 [HNOI2018] 寻宝游戏
    Django 之认证模块
    Django之form表单
    Django 之AJAX
    Django 之中间组件
    Django之 Cookie,Session
    Django之F和Q查询等其他查询
  • 原文地址:https://www.cnblogs.com/zhuanzhuruyi/p/6569227.html
Copyright © 2020-2023  润新知