数据结构基础学习之(内排序)

2019-04-09  本文已影响0人  JiaJianHuang

学习知识

一、 内排序

1. 概念

排序分类

1. 内部排序
2. 外部排序
3. 稳定排序: 若相同关键字间的前后位置关系在排序前与排序后保持一致,则称为稳定排序; 反之为不稳地排序

2. 直接插入排序(插入排序,稳定排序方法)

步骤: 假设待排序的记录存放在数组r[0...n-1]中

  1. 将r[i] 暂存在临时变量temp中
  2. 将temp 与r[j] (j=i-1,i-2,...,0)依次比较,若temp < r[j], 则将r[j]后移,直到temp>=r[j]为止
  3. 将temp插入第(j+1)的位置上
  4. 令i=1,2,..,n重复步骤2,3

示意图

insertion sort.png

代码实现:

 /**
     * 没有设置哨兵的插入排序
     *
     * @param elements
     */
    public void sortNoGuard(int[] elements) {
        int i, j;
        for (i = 1; i < elements.length; i++) {
            //1. 将r[i]存放在临时变量temp中
            int temp = elements[i];
            //2. 将r[j] 与temp比较,当temp <=r[j], 将r[j]往后移
            for (j = i - 1; j >= 0 && temp < elements[j]; j--) {
                elements[j + 1] = elements[j];
            }
            //3. 将temp插入j+1的位置
            elements[j + 1] = temp;
        }
    }

    /**
     * 插入算法改进
     */
    public void sortWithGuard(int[] elements) {
        int i, j;
        int len = elements.length;
        for (i = 1; i < len; i++) {
            //elements[0]充当哨兵,避免边界判断
            elements[0] = elements[i];

            for (j = i - 1;  elements[0] < elements[j]; j--) {
                elements[j + 1] = elements[j];
            }
            //3. 将elements[i]插入j+1的位置
            elements[j + 1] = elements[0];

        }
    }

算法分析

  1. 空间复杂度为: O(1)
  2. 时间复杂度:

3. 希尔排序(插入排序,不稳定排序)

步骤:

  1. 选择一个增量序列{d0,d1,d2,d3,..., dk-1}
  2. 根据当前的增量di将n条记录分成di个子表,每个子表中记录的下标相隔为di
  3. 对各个子表中的记录进行直接插入排序
  4. 令i=1,1,..., k-1, 重复步骤2~4

示意图

shellsort.png

代码实现

/**
     * 希尔排序
     * <p>
     * 时间复杂度: O(nlog2n)
     *
     * @param elements 待排序的序列
     * @param d        增量集合
     */
    public static void sort(int[] elements, int[] d) {
        int i, j;
        for (int k = 0; k < d.length; k++) {

            //对每个子表中的记录进行直接插入排序
            int dk = d[k];
            for (i = 0; i < elements.length; i++) {
                //存临时变量
                int temp = elements[i];
                //移动
                for (j = i - dk; j >= 0 && temp < elements[j]; j -= dk) {
                    elements[j + dk] = elements[j];
                }
                elements[j + dk] = temp;
            }
        }
    }

复杂度分析

  1. 空间复杂度:O(1)
  2. 时间复杂度:O(nlog2n)
4. 冒泡排序(交换排序,稳定)

步骤:

  1. 置初值 i=1
  2. 在无序序列{r[0],r[1],..., r[n-i]}中,从头至尾依次比较相邻的两个记录r[j]与r[j+1] (0<=j<=n-i-1), 若r[j]>r[j+1]则置换位置
  3. i=i+1
  4. 重复步骤2,3

示意图

7.3bubblesort.png

代码实现

/**
     * 冒泡排序
     * <p>
     * 空间复杂度:O(1)
     * 时间复杂度: O(n^2)
     *
     * @param elements
     */
    public void sort(int[] elements) {

        //标识序列是否发生交换
        boolean swap_flag = true;
        for (int i = 1, len = elements.length; i < len && swap_flag; i++) {
            //标识没有发生交换
            swap_flag = false;
            for (int j = 0; j < len - i; j++) {
                if (elements[j] > elements[j + 1]) {
                    int temp = elements[j];
                    elements[j] = elements[j + 1];
                    elements[j + 1] = temp;
                    //标识交换
                    swap_flag = true;
                }
            }
        }
    }

复杂度

  1. 空间复杂度:O(1)
  2. 时间复杂度:
5. 快速排序(交换,不稳定排序)

步骤:(partition 函数)

  1. 设置两个变量i,j, 初始值为low和hight,分别代表待排序序列的起始位置和终止位置
  2. 将第i个记录暂存在变量pivot中,即pivot=r[i]
  3. 从下标j的位置开始由后向前依次搜索,当找到一个比pivot的关键字值小的元素时,则将该元素 替换第 i 个 元素,然后i=i+1
  4. 从下标i的位置开始由前向后依次搜索,当找到一个比pivot的关键字值大的记录时,则将该该元素 替换第 j 个元素,然后j=j+1
  5. 重复步骤:3, 4 步,直到i==j为止
  6. r[i] = pivot

示意图

7.4quicksort.png

代码实现

 /**
     * @param i
     * @param j
     * @return
     */
    public static int partition(int[] elements, int i, int j) {
        //把第i个元素作为支点
        int pivot = elements[i];
        ////从表的两端向中间扫描
        while (i < j) {
            //找到elements[i] <= elements[j]的下标
            while (i < j && pivot <= elements[j]) {
                j--;
            }

            //当elements[i] <= elements[j], 交换位置
            if (i < j) {
                elements[i] = elements[j];
                i++;
            }

            // elements[i] > elements[j]的下标
            while (i < j && pivot > elements[j]) {
                i++;
            }

            //当elements[i] > elements[j], 交换位置
            if (i < j) {
                elements[j] = elements[i];
                j--;
            }
        }
        //支点记录到位
        elements[i] = pivot;
        //返回支点下标
        return i;
    }

    /**
     * 快速排序
     *
     * @param elements 待排序列
     * @param low
     * @param hight
     */
    public static void qSort(int[] elements, int low, int hight) {
        if (low < hight) {
            int pivotloc = partition(elements, low, hight);
            // 低子表排序
            qSort(elements, low, pivotloc - 1);
            //高子表排序
            qSort(elements, pivotloc + 1, hight);
        }
    }

算法分析

6. 直接选择排序(Straight Selection sort不稳定排序)

算法步骤:

  1. 置i值为0
  2. 当i<n-1时,重复下列步骤:

示意图

7.5StraightSelectSort.png

代码实现

public static void straightSelectionSort(int[] elements) {
        int i, j, minIndex;
        //n-1趟排序
        for (i = 0; i < elements.length - 1; i++) {
            //定义最小值索引为 i
            minIndex = i;
            //从i+1开始,找最小值
            for (j = i + 1; j < elements.length; j++) {
                if (elements[j] < elements[minIndex]) {
                    //记录最小值索引
                    minIndex = j;
                }
            }

            //如果最小值索引不等于i, 则与i交换位置
            if (minIndex != i) {
                int temp = elements[i];
                elements[i] = elements[minIndex];
                elements[minIndex] = temp;
            }
            
        }
    }

算法分析

7. 树形选择排序(选择,稳定排序) :解决直接选择排序关键字多次比较问题

基本思想

  1. 首先对n各记录进行两两比较,比较的结果时把关键字值较小者作为优胜者上升为父结点,得到n/2个优胜者,然后再对这n/2个优胜者进行两两比较,重复直到选出一个最小关键字为止。这个过程可以用完全二叉树表示,称为胜者树

算法步骤:

  1. 初始化,令待排序结点的个数为n, 完全二叉树的叶子数leafSize(必须为2的幂), 胜者树所有结点TreeSize = 2* leafSize - 1; 叶结点开始下标loadIndex = leafSize - 1;
  2. 将待排序列复制到胜者树的叶结点中
  3. 构造胜利树,即叶结点两两比较,得到n/2个关键字值小的结点,保存为叶结点的父结点中,再对这些结点进行两两比较,直到得到最小的关键字保存再根结点为止。
  4. 调整胜者树: 先把胜者树的根结点保存到原数组中,再把具有根结点值所对叶子结点的值修改为“最大值”,然后从该结点开始,和其左(右)兄弟结点进行比较,修改从该叶结点到根的路径上各结点的值,直到根结点。
  5. 重复步骤4,直到得到n个结点为止。

示意图

7.6 TreeSelectionSort.png

代码实现

   enum ActiveFlag {
        ACTIVE,//参选
        UNACTIVE;//不参选
    }

    /**
     * 胜者树节点
     */
    class TreeNode {
        //数据
        public int data;
        //该节点的索引
        public int index;
        //标识
        public ActiveFlag active;
    }


    public void treeSelectionSort(int[] elements) {
        //胜者树结点数组
        TreeNode[] tree;
        // 叶子节点数
        int leafSize = 1;

        //得到胜者树的叶子结点数, 该结点数必须为2的幂,才能构造成满二叉树
        while (leafSize < elements.length) {
            leafSize = leafSize * 2;
        }


        //胜者树的所有结点数
        //二叉树的第i层结点数最多: 2^i
        //二叉树的所有结点数最多: 2^k-1 = 2*2^i-1 (k为深度)
        int treeSize = 2 * leafSize - 1;

        // 叶子结点数存放的起始位置
        int loadIndex = leafSize - 1;
        //创建胜者树数组
        tree = new TreeNode[treeSize];
        // 待排序序列下标
        int j = 0;
        //把待排序结点复制到胜者树的叶子结点中
        for (int i = loadIndex; i < treeSize; i++) {
            tree[i] = new TreeNode();
            tree[i].index = i;

            if (j < elements.length) {
                tree[i].active = ActiveFlag.ACTIVE;
                tree[i].data = elements[j++];
            } else {
                tree[i].active = ActiveFlag.UNACTIVE;
            }
        }

        //先进行一次比较查找关键字值最小的结点
        int i = loadIndex;
        while (i > 0) {
            j = i;
            //处理各对比赛对手
            while (j < 2 * i) {
                //完全二叉树的性质:
                //若i=0,则结点为根结点,没有双亲,若i>0,则它的双亲结点编号为 (i-1)/2
                if (tree[j + 1].active == ActiveFlag.UNACTIVE || (tree[j].data <= tree[j + 1].data)) {
                    //左孩子胜者,晋级(成为父节点)
                    tree[(j - 1) / 2] = tree[j];
                } else {
                    //右孩子胜者,晋级(成为父节点)
                    tree[(j - 1) / 2] = tree[j + 1];
                }
                //下一对比赛选手
                j += 2;
            }
            i = (i - 1) / 2;
        }

        //继续找出剩余最小
        for (i = 0; i < elements.length - 1; i++) {
            //将胜者树的根(最小者)存入数组
            elements[i] = tree[0].data;
            //该结点选手落选,不再比赛
            tree[tree[0].index].active = ActiveFlag.UNACTIVE;
            //调整胜者树,再次两两比赛,筛选出最小值
            updateTree(tree, tree[0].index);
        }
        elements[elements.length - 1] = tree[0].data;
    }


    private void updateTree(TreeNode[] tree, int index) {
        // 比赛对手的索引
        int j;

        if (index % 2 == 0) {
            //index 为偶数,对手为左结点
            tree[(index - 1) / 2] = tree[index - 1];
        } else {
            //index 为奇数,对手为右结点
            tree[(index - 1) / 2] = tree[index + 1];
        }
        //最小记录输出,其对手上升到父结点
        index = (index - 1) / 2;
        while (index > 0) {
            if (index % 2 == 0) {
                //index 为偶数,对手为左结点
                j = index - 1;
            } else {
                //index 为奇数,对手为右结点
                j = index + 1;
            }

            //比赛对手中有一个为空
            if (tree[index].active == ActiveFlag.UNACTIVE || tree[j].active == ActiveFlag.UNACTIVE) {
                if (tree[index].active == ActiveFlag.ACTIVE) {
                    //i晋级,
                    tree[(index - 1) / 2] = tree[index];
                } else {
                    //落选
                    tree[(index - 1) / 2] = tree[j];
                }
            } else {
                if (tree[index].data <= tree[j].data) {
                    tree[(index - 1) / 2] = tree[index];
                } else {
                    tree[(index - 1) / 2] = tree[j];
                }
            }
            index = (index - 1) / 2;
        }
    }


算法分析

8. 堆排序(不稳定排序):解决树形排序占空间大问题

基本思想:

  1. 首先将这n条记录按关键字值的大小建成堆(初始堆),将堆顶元素r[0]与r[n-1]交换(或输出);然后,将剩下的r[0]...r[n-2]序调整成堆,再r[0]与r[n-2]交换,又将剩下的r[0]...r[n-3]序列调整成堆,如此反复,便可以得到一个按关键字值有序的记录序列

示意图

7.8两个堆实例及存储结构.png

算法步骤:

  1. 置初始值 i = low,j = 2*i+1,temp = r[i];
  2. 当j < hight时,重复以下操作
    1. 若j<hight-1; r[j] > r[j+1], 则 j ++
    2. 若temp > r[j] , 则r[i] = r[j], j = 2*i+1; 否则j= higth +1
  3. r[i] = temp;
7.9 调整堆.png
  1. 将待排序序列{r[0],r[1],r[2],..., r[n-1]}构造成一棵完全二叉树
  2. 将下标(n/2 - 1) 的记录作为开始调整的子树的根结点
  3. 找出此结点的两个孩子结点中的关键字值较小的,将其与父结点比较,若父结点的关键字值较大,则交换,然后以交换的子结点作为新的父结点,重复此步骤,直到没有子结点为止
  4. 以步骤3 中原来父结点所在位置往前推一个位置,作为新的调整的子树的根结点。继续重复步骤3,直到调整到树根。此时初始堆已形成
  5. 堆建成后,将树根与二叉树的最后一个结点交换后,再将最后一个结点输出(即输出原本的树根),然后比较根结点的两个子结点,若左子结点的关键字值较小,则调整左子树;反之,调整右子树,使它再成为堆。
  6. 重复步骤5,直到二叉树仅剩下一个结点为止
7.10构造堆.png 7.11堆排序过程.png

代码实现:

/**
     * 筛选法调整堆
     * <p>
     * 以low为根结点的子树调整为小顶堆
     */
    public static void sift(int low, int hight, int[] elements) {
        //定义根结点的索引
        int i = low;
        int j = 2 * i + 1;
        //暂时值
        int temp = elements[i];
        //
        while (j < hight) {

            //比较左右子孩子的大小,找出最小的结点
            if (j < hight - 1 && elements[j] > elements[j + 1]) {
                j++;
            }
            //父结点与最小子结点比较,如果父结点大于子节点,则交换位置
            if (temp > elements[j]) {
                elements[i] = elements[j];
                //以j为父结点的子树,再次调整堆
                i = j;
                j = 2 * i + 1;
            } else {
                //退出循环
                j = hight + 1;
            }
        }
        elements[i] = temp;
    }

    /**
     * 堆排序
     *
     * @param elements
     */
    public static void headSort(int[] elements) {
        //创建堆
        int len = elements.length;
        //2. 以下标(n/2-1) 作为开始调整子树的根结点,进行筛选法调整堆
        for (int i = (len / 2 - 1); i >= 0; i--) {
            sift(i, len, elements);
        }
        for (int i = len - 1; i > 0; i--) {
            //将根结点与二叉树最后一个结点交换
            int temp = elements[0];
            elements[0] = elements[i];
            elements[i] = temp;
            //使用筛选法调整堆法,调整为大顶堆
            sift(0, i, elements);
        }
    }

算法分析:

9. 归并排序(Merging Sort, 稳定排序)

基本思想

  1. 将待排序记录r[0] 到r[n-1] 看成是一个含有n个长度为1的有序子表
  2. 把这些子表依次进行两两归并,得到n/2个有序的子表;
  3. 然后,再把这n/2个有序的子表进行两两归并,
  4. 如此重复,直到最后得到一个长度为n的有序表为止

示意图

[图片上传中...(7.12二路归并排序示例.png-978f33-1554628574110-0)]

代码实现

   /**
     * 归并两个相邻的有序表r[h]-r[m] 和r[m+1]-r[t] 归并为r[h]-r[t]
     *
     * @param r     待排序序列
     * @param order 归并后序列
     * @param h     第一个有序表第一个元素的下标
     * @param m     第一个有序表第最后元素的下标
     * @param t     第二个有序表第最后元素的下标
     */
    public void merge(int[] r, int[] order, int h, int m, int t) {
        int i = h, j = m + 1, k = h;

        //将r中两个相邻子序列归并到order中
        while (i <= m && j <= t) {
            //较小值复制到order中
            if (r[i] <= r[j]) {
                order[k++] = r[i++];
            } else {
                order[k++] = r[j++];
            }
        }

        //将前一个子序列剩余元素复制到order中
        while (i <= m) {
            order[k++] = r[i++];
        }

        //将后一个子序列剩余元素复制到order中
        while (j <= t) {
            order[k++] = r[j++];
        }
    }

    /**
     * 归并排序
     *
     * @param r     待排序序列
     * @param order 归并结果序列
     * @param s     待归并有序子序列的长度
     * @param n     待排序序列长度
     */
    public void mergepass(int[] r, int[] order, int s, int n) {
        //定义每对待合并表的第一个元素的下标,初始值为0
        int p = 0;
        //两两归并长度均为s的有序表
        //p: 有序表的开始元素下标,p+s-1: 有序表的结束元素下标
        while (p + 2 * s - 1 <= n - 1) {
            merge(r, order, p, p + s - 1, p + 2 * s - 1);
            p = p + 2 * s;
        }
        //归并最后两个长度不等的有序表
        if (p + s - 1 < n - 1) {
            merge(r, order, p, p + s - 1, n - 1);
        } else {
            //将剩余有序表复制到order中
            for (int i = p; i <= n - 1; i++) {
                order[i] = r[i];
            }
        }
    }


    /**
     * 二路归并算法
     */
    public void mergeSort(int[] elements) {
        int s = 1;
        int len = elements.length;
        int[] temp = new int[len];
        while (s < len) {

            mergepass(elements, temp, s, len);
            s = s * 2;
            mergepass(temp, elements, s, len);
            s = s * 2;
        }
    }

算法分析

10. 基数排序(Radix Sort,稳定排序)

概念:

  1. 基数排序是一种借助于多关键字进行排序

示意图(图来自 skywang12345

7.13 基数排序.png

基本思想

  1. 将一个序列中的逻辑关键字看成由d个关键字复合而成,并采用最低位优先方法对该序列进行多个关键字排序,即从最低关键字开始,将整个序列中的元素“分配”到rd个队列中,再依次“收集”成一个新的序列,如此重复进行d次,即完成排序过程

代码实现:

/**
     * 获取序列最大值
     */
    private static int getMax(int[] array) {
        int max = array[0];
        for (int i = 1; i < array.length; i++)
            if (max < array[i])
                max = array[i];
        return max;

    }

    /**
     * 按对数组按照"某个位数"进行排序
     *
     * @param arr 数组
     * @param n   数组长度
     * @param exp 排序的位数
     */
    private static void countSort(int[] arr, int n, int exp) {
        int output[] = new int[n]; //输出的数组
        int i;
        int count[] = new int[10];

        // 保存出现的次数
        //如:179,589 按个位排序(9) ;在count数组中的最后一个count[9] = 2;
        for (i = 0; i < n; i++)
            count[(arr[i] / exp) % 10]++;

        // 更改count[i]。目的是让更改后的count[i]的值,是该数据在output[]中的位置。
        for (i = 1; i < 10; i++)
            count[i] += count[i - 1];

        // 将数据存储到临时数组output[]中
        for (i = n - 1; i >= 0; i--) {
            output[count[(arr[i] / exp) % 10] - 1] = arr[i];
            count[(arr[i] / exp) % 10]--;
        }

        // 将排序好的数据赋值给a[]
        for (i = 0; i < n; i++)
            arr[i] = output[i];
    }

    public static void radixSort(int arr[]) {
        //获取最大值
        int m = getMax(arr);
        //长度
        int n = arr.length;

        // 从个位开始,对数组a按"指数"进行排序
        for (int exp = 1; m / exp > 0; exp *= 10)
            countSort(arr, n, exp);
    }

11. 总结

  1. 各种内部排序算法的性能比较

  2. 选择排序算法建议:

  1. 各个排序比较


    7.1各种内部排序算法的性能比较.png
上一篇下一篇

猜你喜欢

热点阅读