JavaScript数据结构与算法 - 排序算法

Posted 友人A ㅤ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript数据结构与算法 - 排序算法相关的知识,希望对你有一定的参考价值。

1. 冒泡排序

冒泡排序比较所有相邻的两个项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样。但是,从运行时间的角度来看,冒泡排序是最差的一个。

const Compare = {
    LESS_THAN = -1,
    BIGGER_THAN = 1,
    EQUALS = 0
};

function defaultCompare(a, b) {
    if (a === b) {
        return Compare.EQUALS;
    }
    return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
};

function bubbleSort(array, compareFn = defaultCompare) {
    // 存储数组长度
    const { length } = array;
    // 从数组第一位迭代到最后一位,控制了在数组中经过了多少轮排序
    for (let i = 0; i < length; i++) {
        // 从第一位迭代到倒数第二位,从内循环中减去外循环中已跑过的轮数,可以避免内循环中所有不必要的比较
        for (let j = 0; j < length - 1 - i; j++) {
            // 当前项比下一项大,交换顺序
            if (compareFn(array[j], array[j + 1]) === Compare.BIGGER_THAN) {
                swap(array, j, j + 1);
            }
        }
    }
}

function swap(array, a, b) {
    [array[a], array[b]] = [array[b], array[a]];
}

冒泡排序工作过程:

改进后的冒泡排序:如果从内循环减去外循环中已跑过的轮数,就可以避免内循环中所有不必要的比较

时间复杂度: O(n²)


2. 选择排序

思路:找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。



时间复杂度: O(n²)


3. 插入排序

插入排序:每次排一个数组项,以此方式构建最后的排序数组。

  1. 假定第一项已经排序了
  2. 接着,它和第二项进行比较——第二项是应该待在原位还是插到第一项之前
  3. 接着和第三项比较(它是该插入到第一、第二还是第三的位置)
  4. 以此类推
function insertionSort(array, compareFn = defaultCompare) {
    const { length } = array;
    let temp;
    // 迭代数组给第i项找到正确的位置
    for (let i = 1; i < length; i++) {
        // 用i的值来初始化一个辅助变量
        let j = i;
        // 将i的值存储在临时变量中
        temp = array[i];
        // 只要j比0大,并且数组中前面的值比待比较的值大
        while (j > 0 && compareFn(array[j - 1], temp) === Compare.BIGGER_THAN) {
            // 将这个值移到当前位置上并减小j
            array[j] = array[j - 1];
            j--;
        }
        array[j] = temp;
    }
    return array;
}


时间复杂度: O(n²)


4. 归并排序

归并排序是可以实际使用的排序算法。

归并排序是一种分而治之算法。

算法思想:将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。

function mergeSort(array, compareFn = defaultCompare) {
    // 算法是递归的,需要一个停止条件:判断数组长度是否为1
    if (array.length > 1) {
        const { length } = array;
        // 找到数组的中间位
        const middle = Math.floor(length / 2);
        // 将数组分为两个小数组
        const left = mergeSort(array.slice(0, middle), compareFn);
        const right = mergeSort(array.slice(middle, length), compareFn);

        array = merge(left, right, compareFn);
    }
    return array;
}
// merge函数,负责合并和排序小数组来产生大数组,直到回到原始数组并已排序完成
function merge(left, right, compareFn) {
    // 声明归并过程要创建的新数组和用来迭代两个数组所需的两个变量
    let i = 0;
    let j = 0;
    const result = [];

    // 比较来自left数组的项是否比来自right数组的项小
    while (i < left.length && j < right.length) {
        // 如果是,将该项从left数组添加至归并结果数组,并递增用于迭代数组的控制变量
        // 如果不是,从right数组添加项并递增用于迭代数组的控制变量
        result.push(compareFn(left[i], right[i]) === Compare.LESS_THAN ? left[i++] : right[j++]);
    }
    // 将left数组所有剩余的项添加到归并数组中,right数组也一样
    return result.concat(i < left.length ? left.slice(i) : right.slice(i));
}


可以看到,算法首先将原始数组分割直至只有一个元素的子数组,然后开始归并。

时间复杂度: O(nlog(n))


5. 快速排序

常用算法之一。

快速排序使用分而治之的方法,将原始数组分为较小的数组。

算法思想:

  1. 从数组中选择一个值作为主元(pivot),也就是数组中间的那个值
  2. 创建两个指针(引用),左边一个指向数组第一个值,右边一个指向数组最后一个值
  3. 划分操作:移动左指针直到找到一个比主元大的值,接着,移动右指针直到找到一个比主元小的值,然后交换它们。重复这个过程,直到左指针超过了右指针。这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后
  4. 算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的步骤,直至数组已完全排序
// 声明一个主方法来调用递归函数,传递待排序数组、索引0及其最末的位置作为参数
function quickSort(array, compareFn = defaultCompare) {
    return quick(array, 0, array.length - 1, compareFn);
}

// 创建quick函数
function quick(array, left, right, compareFn) {
    // index能帮助将子数组分离为较小值和较大值数组
    let index;
    // 如果数组长度比1大,就执行partition操作
    if (array.length > 1) {
        index = partition(array, left, right, compareFn);
        // 如果子数组存在较小值的元素,则对该数组重复这个过程
        if (left < index - 1) {
            quick(array, left, index - 1, compareFn);
        }
        // 如果子数组存在较大值的元素,则对该数组重复这个过程
        if (index < right) {
            quick(array, index, right, compareFn);
        }
    }
    return array;
};

// 划分过程
function partition(array, left, right, compareFn) {
    // 选择中间值作为主元
    const pivot = array[Math.floor((right + left) / 2)];
    // 初始化两个指针,为数组第一个元素和最后一个元素
    let i = left;
    let j = right;

    // 两个指针没有相互交错时,就执行划分过程
    while (i <= j) {
        // 移动left指针直到找到一个比主元大的元素
        while (compareFn(array[i], pivot) === Compare.LESS_THAN) {
            i++;
        }
        // 移动right指针直到找到一个比主元小的元素
        while (compareFn(array[j], pivot) === Compare.BIGGER_THAN) {
            j--;
        }
        // 当左指针指向的元素比主元大且右指针指向的元素比主元小,且此时左指针索引没有右指针索引大时
        if (i <= j) {
            // 交换值
            swap(array, i, j);
            // 移动指针,并从外层while处开始重复此过程
            i++;
            j--;
        }
    }
    // 划分操作结束后,返回左指针索引,用来在index = partition(array, left, right, compareFn)处创建子数组
    return i;
}

实现图例:

给定数组[3, 5, 1, 6, 4, 7, 2],划分操作第一次执行:

对有较小值的子数组执行的划分操作:

有较大值的子数组的划分:

对子数组[2, 3, 5, 4]中的较小子数组[2, 3]继续进行划分:

子数组[2, 3, 5, 4]中的较大子数组[5, 4]也继续进行划分:

最终,较大子数组[6, 7]也会进行划分操作,快速排序算法的操作执行完成。


时间复杂度:O(nlog(n)),性能要比其他O(nlog(n))的好。


6. 计数排序

计数排序是分布式排序,是一个整数排序算法。

分布式排序使用已组织好的辅助数据结构(称为桶),然后进行合并,得到排好序的数组。

思路:使用一个用来存储每个元素在原始数组中出现次数的临时数组。在所有元素都计数完成后,临时数组已排好序并可迭代以构建排序后的结果数组。

function countingSort(array) {
    // 如果数组为空或只有一个元素,不需要运行排序算法
    if (array.length < 2) {
        return array;
    }

    // 找到数组中的最大值
    const maxValue = findMaxValue(array);

    // 创建计数数组,从索引0开始直到最大值索引value+1
    const counts = new Array(maxValue + 1);

    // 迭代数组中的每个位置
    array.forEach(element => {
        // 确保递增操作成功。如果counts数组中用来计数某个元素的位置一开始没有用0初始化的话,将其值赋值为0
        if (!counts[element]) {
            counts[element] = 0;
        }
        // 在counts数组中增加元素计数值
        counts[element]++;
    });

    let sortedIndex = 0;
    counts.forEach((count, i) => {
        // 减少计数值直到它为0
        while (count > 0) {
            array[sortedIndex++] = i;
            count--;
        }
    });
    return array;
}

function findMaxValue(array) {
    let max = array[0];
    for (let i = 1; i < array.length; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    return max;
}

时间复杂度:O(n+k),其中k是临时计数数组的大小。


7. 桶排序

桶排序(也被称为箱排序),分布式排序算法。

思路:将元素分为不同的桶(较小的数组),再使用一个简单的排序算法,例如插入排序(用来排序小数组的不错的算法),来对每个桶进行排序。然后,它将所有的桶合并为结果数组。

// 指定需要多少桶来排序数组
function bucketSort(array, bucketSize = 5) {
    if (array.length < 2) {
        return array;
    }
    // 创建桶并将元素分不到不同的桶中
    const buckets = createBuckets(array, bucketSize);
    // 对每个桶执行插入排序算法和将所有的桶合并为排序后的结果数组
    return sortBuckets(buckets);
}

function createBuckets(array, bucketSize) {
    let minValue = array[0];
    let maxValue = array[0];

    // 迭代原数组找到最小值和最大值
    for(let i = 1; i < array.length; i++) {
        if (array[i] < minValue) {
            minValue = array[i];
        } else if (array[i] > maxValue) {
            maxValue = array[i];
        }
    }

    // 计算每个桶中需要分布的元素个数
    const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
    const buckets = [];
    // 初始化每个桶
    for (let i = 0; i < bucketCount; i++) {
        buckets[i] = [];
    }

    // 迭代数组中的每个元素,计算要将元素放到哪个桶中
    for (let i = 0; i < array.length; i++) {
        const bucketIndex = Math.floor((array[i] - minValue) / bucketSize);
        buckets[bucketIndex].push(array[i]);
    }
    return buckets;
}

function sortBuckets(buckets) {
    // 创建一个用做结果数组的新数组,表示数组不会被修改,会返回一个新数组
    const sortedArray = [];
    // 迭代每个可迭代的桶并应用插入排序
    for (let i = 0; i < buckets.length; i++) {
        if (buckets[i] != null) {
            insertionSort(buckets[i]);
            // 将排好序的桶中的所有元素加入结果数组
            sortedArray.push(...buckets[i]);
        }
    }
    return sortedArray;
}


8. 基数排序

基数排序是一个分布式排序算法。

思想:根据数字的有效位或基数将整数分布到桶中。基数是基于数组中值的记数制的。

function radixSort(array, radixBase = 10) {
    if (array.length < 2) {
        return array;
    }
    const minValue = findMinValue(array);
    const maxValue = findMaxValue(array);

    // 从最后一位开始排序所有的数
    let significantDigit = 1;
    // 首先基于最后一位有效位对数字进行排序
    // 在下次迭代时,会基于第二个有效位进行排序,然后第三位,以此类推,直到没有待排序的有效位
    while ((maxValue - minValue) / significantDigit >= 1) {
        array = countingSortForRadix(array, radixBase, significantDigit, minValue);
        significantDigit *= radixBase;
    }
    return array;
}

// 基于有效位(基数)排序
function countingSortForRadix(array, radixBase, significantDigit, minValue) {
    let bucketsIndex = [];
    const buckets = [];
    const aux = [];

    // 基于基数初始化桶(基于十进制就需要十个桶)
    for (let i = 0; i < radixBase; i++) {
        buckets[i] = 0;
    }

    // 基于数组中数的有效位进行计数排序
    for (let i = 0; i < array.length; i++) {
        bucketsIndex = Math.floor(((array[i] - minValue) / significantDigit) % radixBase);
        buckets[bucketsIndex]++;
    }

    // 计算累积结果来得到正确的计数值
    for (let i = 1; i < radixBase; i++) {
        buckets[i] += buckets[i - 1];
    }

    // 计数完成后,要开始将值移回原始数组中
    for (let i = array.length - 1; i >= 0; i--) {
        // 对原始数组中的每个值,再次获取它的有效位并将它的值移到aux数组中
        bucketsIndex = Math.floor(((array[i] - minValue) / significantDigit) % radixBase);
        aux[--buckets[bucketsIndex]] = array[i];
    }

    // 这步可选。将aux数组中的每个值转移到原始数组中
    for (let i = 0; i < array.length; i++) {
        array[i] = aux[i];
    }
    // 除了返回array,也可以直接返回aux数组而不需要赋值它的值
    return array;
}

以上是关于JavaScript数据结构与算法 - 排序算法的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript算法(冒泡排序选择排序与插入排序)

JavaScript 数据结构与算法之美 - 归并排序快速排序希尔排序堆排序

JavaScript数据结构与算法 - 排序算法

JavaScript数据结构与算法 - 排序算法

JavaScript数据结构与算法 - 排序算法

10种经典排序算法的JavaScript实现方法