数据结构与算法学习(一):排序算法

最近在学习数据结构和算法,沈询老师讲到世界上没有一个完美的数据结构和算法,否则就不会出现这么多的数据结构和算法了,所以想学好数据结构和算法,最基本的就是得弄清这个数据结构和算法出现的原因和背景。那用了这么多年的排序算法,他们之间的联系和递进关系是什么样的呢?All In Code。

package org.longtuteng.sort;

import java.util.Arrays;
import java.util.Random;

/**
 * 最近在学习数据结构和算法,沈询老师讲到世界上没有一个完美的数据结构和算法,否则就不会出现这么多的数据结构和算法了,
 * 所以想学好数据结构和算法,最基本的就是得弄清这个数据结构和算法出现的原因和背景,以及他带来了什么,牺牲了什么。
 * 比如我们学习arraylist,知道它基于数组,下标查找很快,增删很慢;于是出现了linkedlist,基于链表的它可以快速增删,
 * 但是查找效率变低了;那么我又想增删快,又想查找快怎么办,我们引入了树,二叉树,二叉排序树,足够了么,我们发现不够,
 * 因为树形结构太复杂了,维护其平衡要付出额外的代价;于是后面又有了跳表skiplist,兼顾了性能要求和复杂性...
 * <br/>
 * 说下自己为什么要回顾下几个经典排序算法,其实就是上面说的知其形不知其然。首先当然是编码啦,在某个风和日丽的下午,我花
 * 了大概3H时间写了下面6个排序,结果能正确运行的只有一个选择排序,苦笑,然后又花了2个小时调试运行和排序效率对比。感慨
 * 下不同排序算法效率差距真的大。附测试结果:(i7 8核 16g)
 * <br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * + 排序算法 + 数组长度一万用时 + 长度十万用时 + 长度一百万用时 + 长度一千万用时 + 长度一亿用时   +<br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * + 冒泡排序 +    225ms     + 19608ms    + 太久         + 太久         + 太久          +<br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * + 选择排序 +    40ms      + 2875ms     + 太久         + 太久         + 太久          +<br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * + 插入排序 +    17ms      + 837ms      + 92161ms     + 太久         + 太久          +<br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * + 希尔排序 +    4ms       + 14ms       + 171ms       + 2400ms      + 32318ms      +<br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * + 并归排序 +    3ms       + 16ms       + 144ms       + 1201ms      + 15014ms      +<br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * + 快速排序 +    3ms       + 17ms       + 128ms       + 1126ms      + 12938ms      +<br/>
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<br/>
 * <br/>
 * 铺垫了这么多,那我们看一个排序算法到底要看什么呢?(这里只做简单介绍,具体可以参考csdn上很多优秀的文章)
 * 1.时间复杂度,空间复杂度。首先的我们会看算法的比较次数,交换次数,是否占用了额外内存空间。
 * 2.是否稳定,通俗的讲,就是我们将数组[3,7,5,5,1]从小到大排列时,会不会交换相同元素5的位置。
 * <br/>
 * 一些概念补充:
 * 什么是逆序度?
 * 我们想将数组[3,7,5,5,1]从小到大排列时,他其中的逆序为[3,1] [7,1] [7,5] [7,5] [5,1] [5,1] 那么这个数组的
 * 逆序度就是6。很显然天然有序的数组,逆序度为0。
 * <br/>
 * ps:写的时候为了统一思路和阅读全部使用了for循环,其中某些可以改用为while循环使代码更加简洁。^_^
 *
 */
public class SortTest {

    /**
     * 冒泡排序
     * 核心:这个位置应该放哪个元素
     * 通过不停的交换来减少逆序度,每次循环不能维持局部有序。
     *
     * @param array
     */
    public static void dubbleSort(int[] array) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < array.length - 1; i++) {
            for (int j = 0; j < array.length - 1; j++) {
                // 将较小的值替换到前面
                if (array[j] > array[j + 1]) {
                    int temp = array[j + 1];
                    array[j + 1] = array[j];
                    array[j] = temp;
                }
            }
        }
        System.out.println("dubbleSort array length:" + array.length
            + ", cost:" + (System.currentTimeMillis() - start));
    }

    /**
     * 选择排序
     * 核心:这个位置应该放哪个元素
     * 优化冒泡排序,减少交换次数,但是没有减少遍历次数,每次循环维持局部有序。
     *
     * @param array
     */
    public static void selectionSort(int[] array) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < array.length - 1; i++) {
            // 选择出本次循环中最小的
            int minIndex = i;
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }
        System.out.println("selectionSort array length:" + array.length
            + ", cost:" + (System.currentTimeMillis() - start));
    }

    /**
     * 插入排序
     * 核心:这个元素应该放哪个位置
     * 将待排序元素位置空出,优化了交换元素的步骤:
     * 1.冒泡和选择排序的交换步骤:int temp = array[j + 1]; array[j + 1] = array[j]; array[j] = temp;
     * 2.插入排序的交换步骤:array[j] = array[j - 1];
     * 插入排序最好情况的时间复杂度优于选择排序。
     *
     * @param array
     */
    public static void insertSort(int[] array) {
        long start = System.currentTimeMillis();
        for (int i = 1; i < array.length; i++) {
            // 把待插入的位置空出来
            int insertValue = array[i];
            int j = i;
            for (; j - 1 >= 0; j--) {
                if (array[j - 1] > insertValue) {
                    array[j] = array[j - 1];
                } else {
                    break;
                }
            }
            array[j] = insertValue;
        }
        System.out.println("insertSort array length:" + array.length
            + ", cost:" + (System.currentTimeMillis() - start));
    }

    /**
     * 希尔排序
     * 核心:这个元素应该放哪个位置
     * 优化插入排序,通过增加步长gap,让插入算法保持在较好情况下的排序效率。希尔排序的效率由步长的计算方式决定。
     *
     * @param array
     */
    public static void shellSort(int[] array) {
        long start = System.currentTimeMillis();
        // 计算增量gap
        for (int g = getKruthGap(array.length); ;g = getKruthGap(g)) {
            for (int i = g; i < array.length; i++) {
                // 把待插入的位置空出来
                int insertValue = array[i];
                int j = i;
                for (; j - g >= 0; j = j - g) {
                    if (array[j - g] > insertValue) {
                        array[j] = array[j - g];
                    } else {
                        break;
                    }
                }
                array[j] = insertValue;
            }
            // 最小增量
            if (g == 1) {
                break;
            }
        }
        System.out.println("shellSort array length:" + array.length
            + ", cost:" + (System.currentTimeMillis() - start));
    }

    /**
     * 希尔排序增量 Kruth算法 K(n) = K(n-1) * 3 + 1 eg: 1, 4, 13, 40, 121...
     *
     * @param pre
     * @return
     */
    private static int getKruthGap(int pre) {
        int n = 1;
        while (n * 3 + 1 < pre) {
            n = n * 3 + 1;
        }
        return n;
    }

    /**
     * 并归排序
     * 核心:递归保证每个最小的元素组,比如两个元素都是有序的,那么整个数组肯定是有序
     * 分治法拆分合并,合并过程需要借助额外内存空间
     *
     * @param array
     */
    public static void mergeSort(int[] array) {
        long start = System.currentTimeMillis();
        mergeSort(array, 0, array.length);
        System.out.println("mergeSort array length:" + array.length
            + ", cost:" + (System.currentTimeMillis() - start));
    }

    /**
     * 排序下标从low 到high 部分元素(包前不包后)<br/>
     * 递归思想:将待排序部分划分为左右两个部分,先将两个部分分别排序,再将两部分合并且排序
     *
     * @param array
     * @param low
     * @param high
     */
    private static void mergeSort(int[] array, int low, int high) {
        if (high - low > 2) {
            int mid = (high - low) / 2 + low;
            // 左边排序
            mergeSort(array, low, mid);
            // 右边排序
            mergeSort(array, mid, high);
            // 将两部分合并且排序
            mergeSort(array, low, mid, high);
        } else if (high - low == 2) {
            // 待排序部分只有两个元素时,直接进行排序,递归结束
            if (array[high - 1] < array[low]) {
                int temp = array[high - 1];
                array[high - 1] = array[low];
                array[low] = temp;
            }
        } else {
            // 待排序部分只有一个元素时,递归结束
            // to do nothing
        }
    }

    /**
     * 合并两部分数组且排序<br/>
     * 原数组的下标从low 到high 部分被mid 划分为左右两个部分,
     * 且两个部分都是有序的,如何使下标从low到high变得整体有序<br/>
     * 简单思考如何将两个数组[1, 4, 8, 10] 和[2, 3, 6, 7] 合并成一个有序的数组
     *
     * @param array
     * @param low
     * @param mid
     * @param high
     */
    private static void mergeSort(int[] array, int low, int mid, int high) {
        // 拷贝一份待排序部分,把array 待排序部分空出来
        // 拷贝整个数组会造成内存浪费,所以只拷贝一部分
        int[] copy = Arrays.copyOfRange(array, low, high);
        // 这里的left 和right 是两部分数组在拷贝数组中对应的起始下标
        int left = 0;
        int right = mid - low;
        // 同样的下文中的left >= mid - low 和right >= high - low是两部分数组在拷贝数组中对应的结束下标
        for(int index = low; index < high; index ++) {
            if (left >= mid - low){
                array[index] = copy[right++];
            } else if (right >= high - low) {
                array[index] = copy[left++];
            } else {
                if (copy[left] < copy[right]) {
                    array[index] = copy[left++];
                } else {
                    array[index] = copy[right++];
                }
            }
        }
    }

    /**
     * 快速排序
     * 核心:递归保证每个元素处于正确位置,比如左边都比他小,右边都比他大,那么整个数组肯定是有序
     * 冒泡算法和并归算法的合并优化,对冒泡算法的交换增加了步长gap,降低交换次数。解决并归算法的合并部分复杂的问题
     *
     * @param array
     */
    public static void quickSort(int[] array) {
        long start = System.currentTimeMillis();
        quickSort(array, 0, array.length);
        System.out.println("quickSort array length:" + array.length
            + ", cost:" + (System.currentTimeMillis() - start));
    }

    /**
     * 数组下标从low 到high 部分元素的哨兵位置设置正确(包前不包后)<br/>
     * 递归思想:设置指数组定段哨兵位置,按哨兵位置将数组划分为左右两部分,分别设置两部分哨兵
     *
     * @param array
     * @param low
     * @param high
     */
    private static void quickSort(int[] array, int low, int high) {
        // 递归结束标志
        if (low == high) {
            return;
        }
        // 设置哨兵, 即标志位,左边比他小,右边比他大
        // 设置左右起始位
        int left = low;
        int right = high - 1;
        // 左右标志位相撞,说明寻找结束
        for (; left < right;) {
            // 右边找一个比哨兵位大的值
            for (; left < right; right--) {
                if (array[right] < array[low]) {
                    break;
                }
            }
            // 左边找一个比哨兵位小的值
            for (; left < right; left++) {
                if (array[left] > array[low]) {
                    break;
                }
            }
            // 交换
            if (left != right) {
                int temp = array[left];
                array[left] = array[right];
                array[right] = temp;
            }
        }
        // 相撞位置即哨兵应在的位置
        int temp = array[low];
        array[low] = array[left];
        array[left] = temp;
        // 分别设置两部分哨兵
        quickSort(array, low, left);
        quickSort(array, left + 1, high);
    }

    /**
     * 创建长度为length的数组
     *
     * @param length
     * @return
     */
    public static int[] createArray(int length) {
        int[] array = new int[length];
        Random random = new Random(System.currentTimeMillis());
        int total = length << 4;
        if (total < 0) {
            total = Integer.MAX_VALUE;
        }
        for (int i = 0; i < length; i++) {
            array[i] = random.nextInt(total);
        }
        return array;
    }

    /**
     * 打印数组内容
     *
     * @param array
     * @return
     */
    public static void print(int[] array) {
        StringBuffer sb = new StringBuffer("[");
        int total = 0;
        for (int i = 0; i < array.length; i++) {
            sb.append(array[i] + " ");
            total += array[i];
        }
        sb.append("] total:" + total);
        System.out.println(sb.toString());
    }

    public static void main(String[] args) {
        int[] array = createArray(23);
        // 1.测试算法是否正确,以希尔排序为例
        print(array);
        shellSort(array);
        print(array);

        // 2.统计算法耗时
        dubbleSort(Arrays.copyOf(array, array.length));
        selectionSort(Arrays.copyOf(array, array.length));
        insertSort(Arrays.copyOf(array, array.length));
        shellSort(Arrays.copyOf(array, array.length));
        mergeSort(Arrays.copyOf(array, array.length));
        quickSort(Arrays.copyOf(array, array.length));
    }
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值