您的位置:首页 > 编程语言 > Java开发

易解排序算法 - 下 (java写:堆排序、快速排序)

2018-03-19 14:26 218 查看

排序算法之堆排序

     堆排序是基于完全二叉树的排序算法,即二叉树的除最后一层外每一层都达到容纳结点的最大值。最后一层从左至右依次排列。堆排序引入了另一种算法设计技巧,使用一种我们称为“堆”的数据结构来进行信息管理,不仅用在堆排序中,而且它也可以构造一种有效的优先队列。
     堆是一个数组,它可以被看成一个类似于完全二叉树的数据结构。即如果我们认为数组下标为 “i” 的位置的元素为根节点,则它的左孩子为 “2 * i” 、右孩子为 “2 * i +1”.如果我们判断结点 ”i“ 的父节点是谁,则“ i / 2” 就是结点“i”的父结点。这里我们计算时采用整型并且默认向下取整。
    堆可以分为两种,一种是“大顶堆”,即根节点必须保证大于左右孩子。另一种是“小顶堆”,即根节点必须保证小于左右孩子。
    这里我们介绍的是堆排序,首先堆排序依照我的理解来看分三块:
            1.堆排序的核心模块:即heapSort函数。
                    (1),构建初始堆
                    (2),倒序循环,从迭代元素=(数组长度 - 1)开始循环直到迭代元素=2执行完毕后跳出循环
                            (2.1)交换第一位 和 数组有效长度最后一位的元素
                            (2.2)从第一位 元素开始向下维护堆的性质
           2.构建初始堆:即createMaxHeap
                   描述:从二叉树最后一个根节点向前遍历,维护出一个具备堆的性质的二叉树。

           3.维护堆的性质:即maintainHeapSort
                  (1.)记录当前结点index的左右孩子的下标,如果左孩子的下标在有效范围内,并且左孩子大于当前结点index, 那么使用largest变量记录下左孩子的下标。否则用largest记录当前结点index的下标
                (2.)如果右孩子的下标在有效范围内,并且右孩子大于当前结点index,那么使用largest变量记录下右孩子的下标。
                (3.)如果 largest !=当前结点的下标,那么交换array[index]和array[largest]。并以largest所记录的孩子的下标为参数调用maintainHeapSort
图解:









源代码:/**
* 堆排序入口,需要传入一个长度大于等于2的数组
* 如果小于2就别排序了,没啥用也
* @param array
* @return
*/
public static Integer[] defaultHeapSort(Integer[] array){
if (array.length<2){
System.out.println("提示:数组长度小于2,由于我们使用1 - n的下标,不使用0 。");
System.out.println("提示:所以一个元素进来一个元素出去我们就别脱裤子放屁了。");
System.out.println("提示:将返回空值,恶心你一下!");
return null;
}
Order.heapSort(array);
return array;
}

/**
* 堆排序
* 构建初始堆
* 替换第一和最后的元素,堆长-1
* 维护堆顶元素和堆的性质
* @param array
* @return
*/
public static Integer[] heapSort(Integer[] array){
Integer heapSize = array.length -1;
Integer temp = null;
Order.createMaxHeap(array,heapSize);
for(Integer i = array.length-1 ; i > 1 ; i--){
temp = array[1];
array[1] = array[i];
array[i] = temp;
heapSize = heapSize - 1;
maintainHeapSort(array,1,heapSize);
}
return array;
}

/**
* 创建初始堆即从最后一个根结点向前遍历
* @param array
* @param heapSize
*/
public static void createMaxHeap(Integer[] array,Integer heapSize){
for(Integer i = heapSize / 2 ; i >0 ; i--){
Order.maintainHeapSort(array,i,heapSize);
}
}

/**
* 维护堆的性质,当前为大顶堆,也就是说左右孩子必须小于根。
* 如果左右孩子不小于根,替换根和左右孩子中大的结点,并对该结点向下继续维护堆性质
* 如果左右孩子都小于根那么就不用继续向下维护
* @param array
* @param index
* @param heapSize
*/
public static void maintainHeapSort(Integer[] array,Integer index,Integer heapSize){
Integer lChild = Order.leftChild(index);
Integer rChild = Order.rightChild(index);
Integer largest = null;
Integer temp = null;
if ( lChild <= heapSize && array[lChild] > array[index]){
largest = lChild;
}else{
largest = index;
}
if(rChild <= heapSize && array[rChild] > array[largest]){
largest = rChild;
}
if (largest != index){
temp = array[largest];
array[largest] = array[index];
array[index] = temp;
maintainHeapSort(array,largest,heapSize);
}
}

/**
* 返回标号为index的结点的左孩子的下标
* @param index
* @return
*/
private static Integer leftChild(Integer index){
return (index * 2);
}

/**
* 返回标号为index的结点的右孩子的下标
* @param index
* @return
*/
private static Integer rightChild(Integer index){
return ((index * 2) + 1);
}

/**
* 返回标号为index的结点的父结点的下标
* @param index
* @return
*/
private static Integer parent(Integer index){
return index / 2;
}

排序算法之快速排序

     快速排序算法是非常使用的算法,能进行原址排序,并且排序的平均时间复杂度为O(nlogn),速度快应用空间小。也是目前主流的排序算法之一,快速排序也采用了类似归并排序的分治思想,以待排序数组最后一位为主元,用主元作为分界点隔开大于或小于主元的数,将数组分为大小两部分,通过递归实现排序。
图解:



源代码: /**
* 快速排序入口
* @param array
* @return
*/
public static Integer[] defaultQuictSortOrder(Integer[] array){
if (array != null)
quictSortOrder(array,0,array.length-1);
return array;
}

/**
* 快速排序
* 传入参数 数组 待排序起点的下标,待排序数组终点的下标
* @param array
* @param pre
* @param rear
* @return
*/
public static Integer[] quictSortOrder(Integer[] array,Integer pre,Integer rear){
Integer midNumber; //记录分为两部分后的中间值 主元所在位置
if (pre < rear){ //如果pre < rear 开始下标小于终点下标

4000
midNumber = quictSortPartition(array,pre,rear); // 返回主元所在位置并把元素分布在主元的左右两边
Order.quictSortOrder(array,pre,midNumber-1); //继续对主元左边进行快速排序
Order.quictSortOrder(array,midNumber+1,rear);//继续对住主元右边进行快速排序
}
return array;
}

/**
* 设定当前部分数组末尾元素为主元
* 把数组元素分布再主元左右两边
* 返回主元所在的位置
* @param array
* @param pre
* @param rear
* @return
*/
public static Integer quictSortPartition(Integer[] array,Integer pre,Integer rear){
Integer mianNumber = array[rear]; // 记录主元的值 设定以数组末尾元素为主元
Integer index = pre - 1; //使用index变量为记录第一个大于主元的元素的前一个位置
Integer temp; //temp用来交换值
for (Integer j = pre; j < rear ; j++){//数组从 pre 遍历到 (rear-1)
if(array[j] <= mianNumber){ //如果array[j]小于主元
index += 1; //我们使 index前进一位
temp = array[index]; //交换array[index]和array[j]
array[index] = array[j]; //描述 : 为什么这样做 首先如果index和j表示的是同一位元素,那么其实没必要交换其值,但是
array[j] = temp;//可能你会问,没必要交换为什么交换。答:这是对算法的一种妥协 举例来看
}
}
/**
* 如果 20 21 22 23 50 24 55 18 30
* 首先以最后一位 30 为主元
* index为-1 pre 为0 rear 为10
* 咱们按照程序流程来走,
* 20小于主元30 index为0 交换 array[index] 和array[j] index和j都为0
* 直到j == 3 也就是遍历到23 index == 3都是相同位置的元素自己和自己交换
* 但是当j走到4,也就是遍历到50
* 50大于主元30 重新执行下一个循环 当前index == 3
* j走到了5 也就是遍历到24 index+1变成了4 index变成了标记大于主元值的位置
* 这时我们交换的是 50 和 24
* 数组变成 20 21 22 23 24 (50) 55 18 30
* J继续向下遍历 此时遍历到55 55大于主元30 那么重新执行循环 当前index为4也就是50的前一个位置
* j遍历到 18 小于主元30 index+1变成了5标记到大于主元的位置,交换50和18
* 数组变成 20 21 22 23 24 18 55 (50) 30 当前index==5标记的是18的下标
* j=rear遍历结束
* 此时18左边全是小于主元的,我们使index++ 变成 6 ,让第6位也就是第一个大于主元的值的元素和主元交换,30和55换
* 数组变成 20 21 22 23 24 18 30 55 50 此时index标记的是30的位置
* 返回 主元下标的位置 index
*/
index++;
temp = array[index];
array[index] = array[rear];
array[rear] = temp;
return index;
}
对比快排和堆排序的时间复杂度:
    测试数据:因处理器的不同所造成的误差性不考虑
                (电脑配置 CPU intel - i7 - 2.80GHz    4核  内存16GB  64位操作系统  windows10)
                十万条数据:

                堆   排序:100000条数据,耗时 101毫秒!   0.101秒
                快速排序:100000条数据,耗时 56毫秒!     0.056秒

                一百万条数据
                    1.测

                        堆排序:1000000条数据,耗时 1335毫秒!   1.335秒

                        快速排序:1000000条数据,耗时 554毫秒!   0.554秒
                    2.测
                        堆排序:1000000条数据,耗时 1433毫秒!   1.433秒
                        快速排序:1000000条数据,耗时 511毫秒!  0.5秒

                一千万条数据
                    1.测

                    堆排序:10000000条数据,耗时 18639毫秒!     18秒

                    快速排序:10000000条数据,耗时 17894毫秒!   17秒
                    2.测
                    堆排序:10000000条数据,耗时 17833毫秒!        17秒
                    快速排序:10000000条数据,耗时 12445毫秒!       12秒

                一亿就不测了,需要跑分钟级别的时间了。。。。
    时间复杂度对比,在排序方面,快速排序大多数情况下都比堆排序快,而且快速排序相对稳定,很难出现最坏的情况。所以平时排序我们经常会使用快速排序而不是堆排序。虽然堆排序也比较快,但是在排序的方面照快排性能上差了很多。
    但是堆这个性质很巧妙,比如我要在一组杂乱无章的数据中取出其中最大或最小,或者是取前多少个最大数字或最小数字,堆这个性质能以最快的速度取出相应的数字。但是快速排序就不能做到想堆这样完美,快速排序需要和二分查找相结合来解决这个问题,提高了问题的复杂性。
    而且堆延伸出来最大优先队列和最小优先队列的概念,大家可以试着去学习理解一下。

总结:

    排序算法有很多种,各个算法的优化也有很多种,但是主要的思想是不会改变的。其实还有排序算法时间复杂度为O(n)的级别,但也都局限于某一个方面,牺牲空间以换取时间。比如桶排序,计数排序,基数排序都有一定的局限性。这里感兴趣的朋友可以自己去网上查阅关于桶排序、计数排序,基数排序的概念及应用场景。(后面的博文我也会继续把这三个牺牲空间换取时间的算法一一实现并叙述一下)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息