JavaScriptのアルゴリズム - ソート

  コンピュータプログラミングでは、アルゴリズムをソートすることは、この記事では、いくつかの一般的なソートアルゴリズムと、それらと複雑さの違いを説明し、最も一般的に使用されるアルゴリズムの一つです。

バブルソート

  バブルソートは、バブルソートは、ソートの原理を説明し始めると例外なく、かかりますが、最も簡単なソートアルゴリズム、およびコンピュータ・プログラミングおよび構造を説明するためのデータのすべてのコースをする必要があります。バブルソートは、さらに理解することは非常に簡単であり、前者が下降すると、昇順のためのものである後者は、スイッチ位置(よりも大きい場合、2つの入れ子ループアレイのペアワイズ比較における要素のアレイを介してですソート、原則を比較)、前者が後者よりも小さいことです。私たちは、達成するためにバブルソートを見て:

関数バブルソート(配列){ 
    長せ = Array.lengthとします。
    以下のために(私に= 0をさせ、私は長さ<; iは++ ){
         ため(J = 0せ; J <長さ- 1; jは++ ){
             もし(配列[J]>配列[J + 1 ]){ 
                [配列[J] 、配列[J + 1] = [配列[J + 1 ]、配列[J]。
            } 
        } 
    } 
}

  上記のコードは、(昇順)古典的なバブルソートではなく、要素の我々の2つの位置の交換は、伝統的な書き込み(一時変数を導入する伝統的な必要性を書いては、二つの変数の値を交換するために使用されている)を使用しないでください、これは新しい機能ES6を使用して、我々は簡単に二つの変数交換の値を達成し、この文法構造を使用することができます。対応する試験結果を見てください:

アレイせ= [];
ため(I = 5せ; I> 0; i-- ){ 
    のArray.push(I)。
} 

にconsole.log(array.toString())。// 5,4,3,2,1 
バブルソート(配列)。
console.log(array.toString())。// 1,2,3,4,5

   最初の内部ループを、次のようにバブルソートでは、内側のループに対して、最大値毎に(昇順に対して)代わりに、この最後のものは、そのプロセスであります最後のアレイを含むアレイの最大を見つけ、第二内部ループ、アレイの最後から2番目の位置にルーティングされ、アレイ内の二番目に大きい値を見つけるために、第3の内部ループ、最初の配列を見つけます。 3つの値がように下部3のアレイに送ら......と。だから、内側のループのために、それぞれの時間は、私たちが行き来することができない長さ- 1点の位置を、だけ横断する必要がある長さ- I - 1つのので、内側のループが横断回数を減らし、その上に位置します。ここでは、改善バブルソートのアルゴリズムは次のとおりです。

関数bubbleSortImproved(アレイ){ 
    長せ = Array.lengthとします。
    (; iが長さ<I ++は、I = 0せ{)
         ため(J ++ jは= 0せ; - - 1 I J <長{)
             もし(配列[J]>配列[J + 1 ]){ 
                [配列[ j]は、配列[J + 1] = [配列[J + 1 ]、配列[J]。
            } 
        } 
    } 
}

  ランニングテストの結果、及び前のバブルソートの結果は、()と同じ方法で得られます。

アレイせ= [];
ため(I = 5せ; I> 0; i-- ){ 
    のArray.push(I)。
} 

にconsole.log(array.toString())。// 5,4,3,2,1 
bubbleSortImproved(アレイ)
console.log(array.toString())。// 1,2,3,4,5

  それはアルゴリズムをソートするプロセスを説明するのが最も簡単ですが、実際には、我々は、バブルソートアルゴリズムを使用することはお勧めしません。バブルソートアルゴリズムの複雑さO(N- 2

選択ソート

  選択ソートとバブルソートは、それが、アレイを横切る2つの入れ子ループを必要とするが、各サイクルにおいて、これは、次いで、場合降順、昇順のためのものである(最小の要素を見つけるために、非常に類似しています私たちは)最大の要素を見つける必要があります。最初の最初の行の最小の要素を見つけるために渡し、第二のパスは2番目の場所の次の最小要素を見つけ、そしてします。私たちは、実装の選択ソートを見て:

機能選択ソート(配列){ 
    長させ = Array.lengthと。
    分をしましょう。

    ため ; - ;(私は+ +1私は長さ<I = 0せ{) = iは、
        (LET J = I; J <長あり、j ++ ){
             もし(配列[分]> 配列[J]){  = J。
            } 
        } 

        もし(I ==!分){ 
            [配列[i]は、配列[分] = [配列[分]、配列[I]]。
        } 
    } 
}

  上記コードが昇順に選択され、その実行は、である最小値minと最初の要素の最初の要素、及び次いで内側ループ内の配列の各要素を介して、要素の値がminよりも小さい場合、値は、素子分に割り当てられます。内層を横断が完了した後にアレイと分間の最初の要素が異なる場合、それらは場所を交換します。次に、最小の素子分のような第2の要素は、前処理を繰り返します。配列の各要素は、比較的完成までです。ここでのテスト結果は以下のとおりです。

アレイせ= [];
ため(I = 5せ; I> 0; i-- ){ 
    のArray.push(I)。
} 

にconsole.log(array.toString())。// 5,4,3,2,1 
選択ソート(配列)。
console.log(array.toString())。// 1,2,3,4,5

  選択ソートアルゴリズムの複雑さと泡として一種であるO(N 2

挿入ソート

  最初の二つのソートアルゴリズムを挿入ソートが同じ考えではなく、理解を容易にするために、我々は[5、4、3、2、1]例として、配列は、挿入ソートの実行の全体にわたって、以下の図によって説明します。

  挿入ソートは、配列のトラバースは、第二の要素から開始され、tmpが現在の要素の位置を保存するための一時的な変数です。値はTMP(目的のための昇順)、この要素の値は、この位置に挿入され、そして最終的に最初のTMPの配列にされているよりも大きければ、素子tmpの前に位置を取るために、現在の位置から開始して、比較されます位置(インデックス番号0)。このプロセスは、配列要素の完全なトラバーサルまで繰り返されます。以下は、ソートアルゴリズムを実装して挿入されます。

function insertionSort(array) {
    let length = array.length;
    let j, tmp;

    for (let i = 1; i < length; i++) {
        j = i;
        tmp = array[i];
        while (j > 0 && array[j - 1] > tmp) {
            array[j] = array[j - 1];
            j--;
        }
        array[j] = tmp;
    }
}

  对应的测试结果:

let array = [];
for (let i = 5; i > 0; i--) {
    array.push(i);
}

console.log(array.toString()); // 5,4,3,2,1
insertionSort(array);
console.log(array.toString()); // 1,2,3,4,5

  插入排序比冒泡排序和选择排序算法的性能要好。

归并排序

  归并排序比前面介绍的几种排序算法性能都要好,它的复杂度为O(nlogn)

  归并排序的基本思路是通过递归调用将给定的数组不断分割成最小的两部分(每一部分只有一个元素),对这两部分进行排序,然后向上合并成一个大数组。我们还是以[ 5, 4, 3, 2, 1 ]这个数组为例,来看下归并排序的整个执行过程:

  首先要将数组分成两个部分,对于非偶数长度的数组,你可以自行决定将多的分到左边或者右边。然后按照这种方式进行递归,直到数组的左右两部分都只有一个元素。对这两部分进行排序,递归向上返回的过程中将其组成和一个完整的数组。下面是归并排序的算法的实现:

const merge = (left, right) => {
    let i = 0;
    let j = 0;
    const result = [];

    // 通过这个while循环将left和right中较小的部分放到result中
    while (i < left.length && j < right.length) {
        if (left[i] < right[i]) result.push(left[i++]);
        else result.push(right[j++]);
    }

    // 然后将组合left或right中的剩余部分
    return result.concat(i < left.length ? left.slice(i) : right.slice(j));
};

function mergeSort(array) {
    let length = array.length;
    if (length > 1) {
        const middle = Math.floor(length / 2); // 找出array的中间位置
        const left = mergeSort(array.slice(0, middle)); // 递归找出最小left
        const right = mergeSort(array.slice(middle, length)); // 递归找出最小right
        array = merge(left, right); // 将left和right进行排序
    }
    return array;
}

  主函数mergeSort()通过递归调用本身得到left和right的最小单元,这里我们使用Math.floor(length / 2)将数组中较少的部分放到left中,将数组中较多的部分放到right中,你可以使用Math.ceil(length / 2)实现相反的效果。然后调用merge()函数对这两部分进行排序与合并。注意在merge()函数中,while循环部分的作用是将left和right中较小的部分存入result数组(针对升序排序而言),语句result.concat(i < left.length ? left.slice(i) : right.slice(j))的作用则是将left和right中剩余的部分加到result数组中。考虑到递归调用,只要最小部分已经排好序了,那么在递归返回的过程中只需要把left和right这两部分的顺序组合正确就能完成对整个数组的排序。

  对应的测试结果:

let array = [];
for (let i = 5; i > 0; i--) {
    array.push(i);
}

console.log(array.toString()); // 5,4,3,2,1
console.log(mergeSort(array).toString()); // 1,2,3,4,5

快速排序

  快速排序的复杂度也是O(nlogn),但它的性能要优于其它排序算法。快速排序与归并排序类似,其基本思路也是将一个大数组分为较小的数组,但它不像归并排序一样将它们分割开。快速排序算法比较复杂,大致过程为:

  1. 从给定的数组中选取一个参考元素。参考元素可以是任意元素,也可以是数组的第一个元素,我们这里选取中间位置的元素(如果数组长度为偶数,则向下取一个位置),这样在大多数情况下可以提高效率。
  2. 创建两个指针,一个指向数组的最左边,一个指向数组的最右边。移动左指针直到找到比参考元素大的元素,移动右指针直到找到比参考元素小的元素,然后交换左右指针对应的元素。重复这个过程,直到左指针超过右指针(即左指针的索引号大于右指针的索引号)。通过这一操作,比参考元素小的元素都排在参考元素之前,比参考元素大的元素都排在参考元素之后(针对升序排序而言)。
  3. 以参考元素为分隔点,对左右两个较小的数组重复上述过程,直到整个数组完成排序。

  下面是快速排序算法的实现:

const partition = (array, left, right) => {
    const pivot = array[Math.floor((right + left) / 2)];
    let i = left;
    let j = right;

    while (i <= j) {
        while (array[i] < pivot) {
            i++;
        }
        while (array[j] > pivot) {
            j--;
        }
        if (i <= j) {
            [array[i], array[j]] = [array[j], array[i]];
            i++;
            j--;
        }
    }
    return i;
};

const quick = (array, left, right) => {
    let length = array.length;
    let index;
    if (length > 1) {
        index = partition(array, left, right);
        if (left < index - 1) {
            quick(array, left, index - 1);
        }
        if (index < right) {
            quick(array, index, right);
        }
    }
    return array;
};

function quickSort(array) {
    return quick(array, 0, array.length - 1);
}

  假定数组为[ 3, 5, 1, 6, 4, 7, 2 ],按照上面的代码逻辑,整个排序的过程如下图所示:

  下面是测试结果:

let array = [3, 5, 1, 6, 4, 7, 2];
console.log(array.toString()); // 3,5,1,6,4,7,2
console.log(quickSort(array).toString()); // 1,2,3,4,5,6,7

  快速排序算法理解起来有些难度,可以按照上面给出的示意图逐步推导一遍,以帮助理解整个算法的实现原理。

堆排序

  在计算机科学中,堆是一种特殊的数据结构,它通常用树来表示数组。堆有以下特点:

  • 堆是一棵完全二叉树
  • 子节点的值不大于父节点的值(最大堆),或者子节点的值不小于父节点的值(最小堆)
  • 根节点的索引号为0
  • 子节点的索引为父节点索引 × 2 + 1
  • 右子节点的索引为父节点索引 × 2 + 2

  堆排序是一种比较高效的排序算法。

  在堆排序中,我们并不需要将数组元素插入到堆中,而只是通过交换来形成堆,以数组[ 3, 5, 1, 6, 4, 7, 2 ]为例,我们用下图来表示其初始状态:

  那么,如何将其转换成一个符合标准的堆结构呢?先来看看堆排序算法的实现:

const heapify = (array, heapSize, index) => {
    let largest = index;
    const left = index * 2 + 1;
    const right = index * 2 + 2;
    if (left < heapSize && array[left] > array[index]) {
        largest = left;
    }
    if (right < heapSize && array[right] > array[largest]) {
        largest = right;
    }
    if (largest !== index) {
        [array[index], array[largest]] = [array[largest], array[index]];
        heapify(array, heapSize, largest);
    }
};

const buildHeap = (array) => {
    let heapSize = array.length;
    for (let i = heapSize; i >= 0; i--) {
        heapify(array, heapSize, i);
    }
};

function heapSort(array) {
    let heapSize = array.length;
    buildHeap(array);

    while (heapSize > 1) {
        heapSize--;
        [array[0], array[heapSize]] = [array[heapSize], array[0]];
        heapify(array, heapSize, 0);
    }

    return array;
}

  函数buildHeap()将给定的数组转换成堆(按最大堆处理)。下面是将数组[ 3, 5, 1, 6, 4, 7, 2 ]转换成堆的过程示意图:

  在函数buildHeap()中,我们从数组的尾部开始遍历去查看每个节点是否符合堆的特点。在遍历的过程中,我们发现当索引号为6、5、4、3时,其左右子节点的索引大小都超出了数组的长度,这意味着它们都是叶子节点。那么我们真正要做的就是从索引号为2的节点开始。其实从这一点考虑,结合我们利用完全二叉树来表示数组的特性,可以对buildHeap()函数进行优化,将其中的for循环修改为下面这样,以去掉对子节点的操作。

for (let i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
    heapify(array, heapSize, i);
}

  从索引2开始,我们查看它的左右子节点的值是否大于自己,如果是,则将其中最大的那个值与自己交换,然后向下递归查找是否还需要对子节点继续进行操作。索引2处理完之后再处理索引1,然后是索引0,最终转换出来的堆如图中的4所示。你会发现,每一次堆转换完成之后,排在数组第一个位置的就是堆的根节点,也就是数组的最大元素。根据这一特点,我们可以很方便地对堆进行排序,其过程是:

  • 将数组的第一个元素和最后一个元素交换
  • 减少数组的长度,从索引0开始重新转换堆

  直到整个过程结束。对应的示意图如下:

  堆排序的核心部分在于如何将数组转换成堆,也就是上面代码中buildHeap()和heapify()函数部分。

  同样给出堆排序的测试结果:

let array = [3, 5, 1, 6, 4, 7, 2];
console.log(array.toString()); // 3,5,1,6,4,7,2
console.log(heapSort(array).toString()); // 1,2,3,4,5,6,7

有关算法复杂度

  上面我们在介绍各种排序算法的时候,提到了算法的复杂度,算法复杂度用大O表示法,它是用大O表示的一个函数,如:

  • O(1):常数
  • O(log(n)):对数
  • O(log(n) c):对数多项式
  • O(n):线性
  • O(n2):二次
  • O(nc):多项式
  • O(cn):指数

  我们如何理解大O表示法呢?看一个例子:

function increment(num) {
    return ++num;
}

  对于函数increment(),无论我传入的参数num的值是什么数字,它的运行时间都是X(相对于同一台机器而言)。函数increment()的性能与参数无关,因此我们可以说它的算法复杂度是O(1)(常数)。

  再看一个例子:

function sequentialSearch(array, item) {
    for (let i = 0; i < array.length; i++) {
        if (item === array[i]) return i;
    }
    return -1;
}

  函数sequentialSearch()的作用是在数组中搜索给定的值,并返回对应的索引号。假设array有10个元素,如果要搜索的元素排在第一个,我们说开销为1。如果要搜索的元素排在最后一个,则开销为10。当数组有1000个元素时,搜索最后一个元素的开销是1000。所以,sequentialSearch()函数的总开销取决于数组元素的个数和要搜索的值。在最坏情况下,没有找到要搜索的元素,那么总开销就是数组的长度。因此我们得出sequentialSearch()函数的时间复杂度是O(n),n是数组的长度。

  同理,对于前面我们说的冒泡排序算法,里面有一个双层嵌套的for循环,因此它的复杂度为O(n2)。

  时间复杂度O(n)的代码只有一层循环,而O(n2)的代码有双层嵌套循环。如果算法有三层嵌套循环,它的时间复杂度就是O(n3)。

  下表展示了各种不同数据结构的时间复杂度:

数据结构 一般情况 最差情况
插入 删除 搜索 插入 删除 搜索
数组/栈/队列 O(1) O(1) O(n) O(1) O(1) O(n)
链表 O(1) O(1) O(n) O(1) O(1) O(n)
双向链表 O(1) O(1) O(n) O(1) O(1) O(n)
散列表 O(1) O(1) O(1) O(n) O(n) O(n)
BST树 O(log(n)) O(log(n)) O(log(n)) O(n) O(n) O(n)
AVL树 O(log(n)) O(log(n)) O(log(n)) O(log(n)) O(log(n)) O(log(n))

数据结构的时间复杂度

 

节点/边的管理方式 存储空间 增加顶点 增加边 删除顶点 删除边 轮询
领接表 O(| V | + | E |) O(1) O(1) O(| V | + | E |) O(| E |) O(| V |)
邻接矩阵 O(| V |2) O(| V |2) O(1) O(| V |2) O(1) O(1)

图的时间复杂度  

 

算法(用于数组) 时间复杂度
最好情况 一般情况 最差情况
冒泡排序 O(n) O(n2) O(n3)
选择排序 O(n2) O(n2) O(n2)
插入排序 O(n) O(n2) O(n2)
归并排序 O(log(n)) O(log(n)) O(log(n))
快速排序 O(log(n)) O(log(n)) O(n2)
堆排序 O(log(n)) O(log(n)) O(log(n))

排序算法的时间复杂度

おすすめ

転載: www.cnblogs.com/jaxu/p/11382646.html
おすすめ