js算法初窥02(排序算法02-归并、快速以及堆排序)

  上一篇,我们讲述了一些简单的排序算法,其实说到底,在前端的职业生涯中,不涉及node、不涉及后台的情况下,我目前还真的没想到有哪些地方可以用到这些数据结构和算法,但是我在前面的文章也说过了。或许你用不到,但是,真的,如果你想要在前端领域有一个不错的发展。数据结构和算法一定是你的必修课。它不仅仅让你在处理问题的时候可以有一个思维底蕴,更重要的是,在遇到一些奇葩产品的时候,你可以和他PK到底!嗯,到底!

  哈哈,开个小玩笑。咱们还是聊点有营养的。上一篇的算法比较简单,主内容就是循环,次内容就是比较。但是,这篇文章所介绍的一些算法,在实现上有些许的复杂,甚至在其中涉及到了一部分数据结构相关的知识,如果你对数据结构还不是十分了解,请移步这里用js来实现那些数据结构—目录

  那么,我们这篇文章要一起来看看另外一些在执行上有着更高效率的算法,比如归并排序,比如快速排序,还有堆排序等等。

1、归并排序

  我们先来看看什么是归并排序,以及归并排序是怎么实现的。

  归并排序属于一种分治算法。归并排序的思想就是将原始数组切分成一个一个较小的数组,直到每一个数组只有一个元素为止,然后再把一个一个小数组,一点一点的结合成一个最终排序后的数组。其实简单来说,就是先分,再合。归并排序的实现有两种方法,一种是递归,一种是迭代。下面我们只用递归的方式来实现一下代码:

  

//归并排序
// 不多说,你懂的。
this.mergeSort = function () {
    array = mergeSortRec(array);
};
//这个私有函数,其实就是整个归并排序中“分”的部分。我们来看看它是如何“分”的。
var mergeSortRec = function (array) {
    //存储数组长度。
    var length = array.length;
    //由于是递归,所以当length 为 1 的时候,说明我们分到底了。直接返回这个数组。也就是只有一个元素的数组。
    //同时这个判断条件也是递归的终止条件,要记住任何递归操作都必须有终止条件。不然会陷入死循环。
    if(length === 1) {
        return array;
    }
    //我们要把原始数组从中一分为二。下面就是一分为二的操作。无需多说。
    var mid = Math.floor(length / 2),
        left = array.slice(0,mid),
        right = array.slice(mid,length);
    // 这里,我们先不去管merge函数是做什么的。我们先看递归到最底层。merge的两个参数会变成什么。
    // 由于我们又返回了自身,至此递归就形成了。在merge的参数中我们又递归调用了一次自身。
    // 那么这次调用我们把left和right两个数组又拆分了一次。直到最后array.length 为 1(归并的最小单位)。
    // 那么换句话说,实际上merge函数递归的最底层传入的就是两个只有一个元素的数组。
    return merge(mergeSortRec(left),mergeSortRec(right));
};

var merge = function (left,right) {
    // 我们声明一个最终结果的数组result,
    // il和ir是用来控制左右两个数组的迭代的变量
    var result = [],il = 0,ir = 0;

    // 这里,我们的最底层是只有两个只有一个元素的数组
    /*
        array[left]和array[right]
        第一个while, 循环的条件是两个长度变量是否在合法值内。
    */
    while(il < left.length && ir < right.length) {
        // 如果左侧小于右侧,此时的il和ir是相等的都是0。注意这一点
        // 我们就把左侧的left[il]放入数组并il++。否则,我们就把right[ir]存入数组result并ir++。此时,il和ir就不相等了。
        // 所以,这时候,我们下一次循环判断的条件就是ir或il的递增与没有递增的il或者ir做比较。这样就做到了一个元素与另外数组中所有元素都比较过的一个方法。
        // 希望上面我说明白了想要表达的意思。
        if(left[il] < right[ir]) {
            // 这里,不太容易理解。为什么我们要在result中加入il++而不是il?
            // 其实这里的意思是,先加如left[il]再il++。
            // 不信,你可以把代码改成这个样子
            /*
            result.push(left[il]);
            il++
            */
            // 效果是一样一样的。
            result.push(left[il++]);
        } else {
            result.push(right[ir++]);
        }
    };
    //这两个循环的目的是把剩余的数组元素(包括left数组和right数组)都存入result数组中。
    // 这样我们就行了一个归并后的结果数组,然后进行下一次的归并过程的初始参数。
    while(il < left.length) {
        result.push(left[il++]);
    };

    while(ir < right.length) {
        result.push(right[ir++]);
    };

    return result;
};

//我们用上一章的方法来测试一下归并排序
var arraylist = creatArrayList(5);
console.log(arraylist.toString());//5,4,3,2,1
arraylist.mergeSort();
console.log(arraylist.toString());//1,2,3,4,5

  其实归并排序的核心思想就是先拆分数组直至分为只有一个元素的数组时,再对其一点一点的进行合并。

2、快速排序

  快速排序是我们在组织架构或者实际应用中最为常用的排序算法之一,你也可以把快速排序用在你的项目中。

  快速排序的思想和归并排序有些类似,但是快速排序不需要把拆分开的元素装入一个新的数组,而是直接在原数组上进行交换操作。

  那么我们来看看快速排序的操作步骤:

    首先我们要找到用于与之相比较的“主元”。理论上主元可以为数组中的任意的元素,但是在本文中我们选取数组的中间项作为主元。

    然后,我们选择主元左侧和右侧的两部分中的元素分别和主元做比较,直到我们找到了左侧比主元大并且右侧比主元小的元素,那么我们就交换两个元素的位置。直到迭代的左侧指针超过了右侧指针。这样,我们就使比主元小的元素和比主元大的元素分别存在于主元的两侧。

    最后,我们再对左右两侧的数组递归重复上面的步骤。直至数组排序结束。

  我们来看下代码。

//快速排序
this.quickSort = function () {
    // 这里传入的参数是原始数组本身和首尾元素的下标。
    quick(array,0,array.length - 1);
};

var quick = function (array,left,right) {
    // 这个index是为了帮助我们分离出较小值数组和较大值数组的
    var index;
    // 如果length>1才执行逻辑,因为只有一个元素的数组意味着无需排序。
    if(array.length > 1) {
        // partition返回一个元素的下标。
        index = partition(array,left,right);
        //下面两个判断条件为了区分我们要递归的是较小值数组还是较大值数组。
        if(left < index - 1) {
            quick(array,left,index - 1);
        };

        if(index < right) {
            quick(array,index,right);
        };
    }
};
//我们来看看划分过程的这个方法
var partition = function (array,left,right) {
    // pivot(主元),也就是我们要与之比较的元素。我们选取中间项作为主元。
    // i和j代表着较小值数组和较大值数组当前元素的指针
    var pivot = array[Math.floor((right + left) / 2)],i = left,j = right;
    //要知道最开始的i是0,j是lenght-1。所以i++是往右侧移动指针,j--是往左侧移动指针。
    //循环的条件是较小值数组的下标小于较大值数组的下标。
    while(i <= j) {
        // 如果array[i]元素小于主元,向右移动指针。
        while(array[i] < pivot) {
            i++;
        };
        // 如果array[j]元素大于主元,向左移动指针。
        while(array[j] > pivot) {
            j--;
        };
        //上面两个while迭代,当遇到不符合条件的时候就会停下来。
        // 而这种不符合条件的情况是array[i]>array[j]。这是要调整的情况。但是此时i仍旧是小于等于j的。要注意,i在这里不可能比j大。
        // 所以我们此时交换两个下标对应的元素,并改变i和j的指针。最后返回下标i。
        if(i <= j) {
            swap(array,i,j);
            i++;
            j--;
        };
    };

    return i;
};

var arraylist = creatArrayList(5);
console.log(arraylist.toString());//5,4,3,2,1
arraylist.quickSort();
console.log(arraylist.toString());//1,2,3,4,5

    

3堆排序

  在说堆排序之前,我们得先了解一下什么是,堆这种数据结构本质是一个完全二叉树(如果你还不知道什么是树,请看我前面的文章),那么既然堆是一种树,那么有关于树的一些概念都可以用在堆上面,比如深度,比如节点等。要知道,树通常都会有左孩子又孩子节点和父节点的指针,但是在完全二叉树中,这些指针都可以去掉,因为我们可以用一定的规律来直接找到当前节点的关联节点。比如给定某一个结点,假设它的下标为i,那么它的左孩子结点的下标就是2i + 1,右孩子结点的下标就是2i + 2,它的父结点为(i?1)/2。这样,我们就把可以省略去这些指针,直接将堆中的结点存储在数组中了。

  在了解了什么是堆之后,我们看看堆排序是怎么操作的。我们直接从代码中看比较具体:

//堆排序
// 这里你需要对树数据结构有一定的了解和认识。如果你对树结构还不是十分了解,请看我前面有关于树结构的相关章节。
// 或者,你可以直接用下面的代码来解决问题,当然,你需要做一些细微的改动。
this.heapSort = function () {
    // 把数组长度存入一个变量
    var heapSize = array.length;
    // 把该数组“堆”化。
    buildHeap(array);
    //迭代递减heapSize的长度,交换数组中下标为0和当前的heapSize重新使变动的堆“合理化”。这里的合理化是指让交换了元素位置的数组重新生成符合堆原理的一个新数组。
    while(heapSize > 1) {
        heapSize--;
        swap(array,0,heapSize);
        heapify(array,heapSize,0);
    };
};
//生成堆函数,我们的i为数组中间值并且递减i的循环为heapify函数传入。
var buildHeap = function (array) {
    var heapSize = array.length;
    for(var i = Math.floor(array.length / 2);i >= 0; i--) {
        heapify(array,heapSize,i);
    };
};
//“堆”化函数。
var heapify = function (array,heapSize,i) {
    //我们声明左节点,右节点,以及父节点(也就是largest)的变量
    var left = i * 2 + 1,
        right = i * 2 + 2,
        largest = i;
    //这两个判断是为了知道在当前轮中的父节点是谁。
    if(left < heapSize && array[left] > array[largest]) {
        largest = left;
    };

    if(right < heapSize && array[right] > array[largest]) {
        largest = right;
    };
    //如果largest变了,我们就需要交换两个值得位置。并且重新调用heapify。
    if(largest !== i) {
        swap(array,i,largest);
        heapify(array,heapSize,largest);
    };
};

var arraylist = creatArrayList(125);
console.log(arraylist.toString());//5,4,3,2,1
arraylist.heapSort();
console.log(arraylist.toString());//1,2,3,4,5

  堆排序的概念其实并不难理解,唯一需要注意的就是堆数据结构的概念,希望我说清楚了,如果你觉得我对于堆的讲解并不详细,首先你可以百度,其次你可以谷歌,再次你还可以去搜维基百科。实在不行,你去这里看看https://zhuanlan.zhihu.com/p/25820535。这是一篇有关于java相关的系列教程之一。当然,就算你不懂java,相信你也一样可以看懂,真的,我没开玩笑。

  我在纠结要不要画张图还是就这样结束,因为画一个完整的流程图真的很花时间......我抽根烟考虑下......要不大家去买本书看吧。。。。嗯....一个不错的建议。 

  下面我们以数组[3,5,1,6,4,7,2]作为图例的基本数据:

  图片要结合代码一起看,不然看不懂的噢.......还有,这个图你最好放大了看,不然累眼睛。。。

  最后,其实有关于排序算法还有很多,比如计数排序,桶排序,基数排序等等等等等。排序算法远不止如此。但是本系列不会介绍这么多的算法,如果你想要更深入的去了解其它算法的内容,可以自行查找相关的资料。

  好了,终于要结束了,希望本文的内容能带给你一点点的收获。

  最后,由于本人水平有限,能力与大神仍相差甚远,若有错误或不明之处,还望大家不吝赐教指正。非常感谢!

原文地址:https://www.cnblogs.com/zaking/p/9053668.html

时间: 2024-10-24 02:26:48

js算法初窥02(排序算法02-归并、快速以及堆排序)的相关文章

冒泡排序算法和简单选择排序算法的js实现

之前已经介绍过冒泡排序算法和简单选择排序算法和原理,现在有Js实现. 冒泡排序算法 let dat=[5, 8, 10, 3, 2, 18, 17, 9]; function bubbleSort(data) { for(let i=0;i<data.length-1;i++){ for(let j=0;j<data.length-1-i;j++){ if(data[j]>data[j+1]){ [data[j],data[j+1]]=[data[j+1],data[j]]; } } }

详谈排序算法之选择类排序(两种方法实现堆排序)

   今天我们再来讨论一下选择类排序,选择类排序分为:简单排序,树形选择排序和堆排序.但我们主要说的是简单和堆排序两个,因为树形选择排序使用了较多的辅助空间,以及和∞进行多余比较,为弥补树型选择排序的这些缺点, J.W.J.Williams 在 1964 年提出了进一步的改进方法,即堆排序.对于我个人而言..一开始并不是很理解它的算法思想,纠结了许久.在网上查找资料的时候发现这位大神的文章思路十分清晰,而且把创建堆以及堆化数组的算法讲解的十分详细.如果有不明白堆排序思路的,可以先看看这篇文章~堆

基本算法研究1-冒泡排序算法测试

基本算法研究1-冒泡排序算法测试 1.经典冒泡排序法基本原理 先看一个动态图,感觉比较形象: 冒泡排序(Bubble Sort)是一种简单的排序算法.默认是从小到大排序,即把最大的数据排在最后,相当于每次把最大数据像气泡一样浮到水面一样.它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数列的工作是重复地进行直到没有再需要交换. 基本步骤: 1.比较相邻的元素.如果第一个比第二个大,就交换他们两个.        2.对每一对相邻元素作同样的工作,从开始第一对

【JavaScript】【算法】JavaScript版排序算法

JavaScript版排序算法:冒泡排序.快速排序.插入排序.希尔排序(小数据时,希尔排序会比快排快哦) 1 //排序算法 2 window.onload = function(){ 3 var array = [0,1,2,44,4, 4 324,5,65,6,6, 5 34,4,5,6,2, 6 43,5,6,62,43, 7 5,1,4,51,56, 8 76,7,7,2,1, 9 45,4,6,7,8]; 10 //var array = [4,2,5,1,0,3]; 11 array

链表插入和删除,判断链表是否为空,求链表长度算法的,链表排序算法演示——C语言描述

关于数据结构等的学习,以及学习算法的感想感悟,听了郝斌老师的数据结构课程,其中他也提到了学习数据结构的或者算法的一些个人见解,我觉的很好,对我的帮助也是很大,算法本就是令人头疼的问题,因为自己并没有学习过算法的系统性的课程,现在还是处于不断摸索的阶段,好多算法题目根本就没有什么思路,导致自己对好多题目都很是头疼,就算是自己做过的一些算法的题目,再次遇到也还是不一定会做出来,他给出的建议就是,看懂别人的程序,然后自己去敲,一定会出错,然后调试,有错误接着调试,一直到没有错误为止,并且要时常的去复习

算法导论专题一--排序算法

排序算法作为许多程序的中间步骤,是计算机科学中的一个基本操作. 一.问题描述 排序算法输入的是n个数的一个序列<a1,a2…..an>,输出为输入的一个排列<a1’,…..an’>,满足a1’<a2’<….<an’ 简言之就是输入一个序列,输出的是这个数组元素从小到大排列的另一序列. 二.方法思想综述 从算法导论这本书上根据算法的复杂度可以将排序算法分为三种,,.,这两种方法都需要数据间的比较,而不需要. 其中有三种为选择,冒泡,插入. 选择排序:最直观,简单但是

算法导论专题一--排序算法(2)

上节分析了O(n^2)的算法,这节就分析O(nlgn)的算法-归并,快速和堆排序. 一:综述 O(nlgn) 的算法可以分为两大类,两者所用的技术差别较大.归并和快速排序采用的是分治策略,这两者相当于一个对称的过程,一个是自顶向上合并子问题,另一个则自上向下分解子问题.而堆排序利用堆这一数据结构元素间的特殊关系来排序一个序列,另外采用二叉树的方式组织数据使其效率大大提高. 二:分治策略排序算法 1.为什么使用分治? 在上节算法的分析中,不管是冒泡.选择还是插入都不适用于大规模的数据,因为数据一大

python数据结构与算法第八天【排序算法】

1.排序算法的稳定性 稳定排序算法会让原本有相同键值的记录维持相对次序 例如:对以下元组按照元组的第一个元素升序排列,元组如下: (4,1) (3,1) (3,7) (5,6) 若要满足条件,则可能的排序有: 情况一: (3,1) (3,7) (4,1) (5,6) 情况二: (3,7) (3,1) (4,1) (5,6) 虽然情况一和情况二都是满足条件的,但是情况二在满足条件下打破了原本无需改变的顺序 原文地址:https://www.cnblogs.com/liuzhiqaingxyz/p/

数据结构与算法系列十(排序算法概述)

1.引子 1.1.为什么要学习数据结构与算法? 有人说,数据结构与算法,计算机网络,与操作系统都一样,脱离日常开发,除了面试这辈子可能都用不到呀! 有人说,我是做业务开发的,只要熟练API,熟练框架,熟练各种中间件,写的代码不也能“飞”起来吗? 于是问题来了:为什么还要学习数据结构与算法呢? #理由一: 面试的时候,千万不要被数据结构与算法拖了后腿 #理由二: 你真的愿意做一辈子CRUD Boy吗 #理由三: 不想写出开源框架,中间件的工程师,不是好厨子 1.2.如何系统化学习数据结构与算法?