排序算法之javascript版

221 阅读8分钟

  一直觉得做开发这一行,数据结构和算法是基础,基础不牢固,这一行注定走的不会太远。所以最近也是在花时间重新来学一下数据结构和算法,并以 javascript 来将这些算法来实现一遍,并在此学习记录下来。

1、基本排序算法

这几种排序方法的时间复杂度都是O(n^2)

1、冒泡排序(Bubble Sort)

  是最慢的排序算法之一,也是一种最容易实现的排序算法。
基本原理:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。是一种交换排序。冒泡排序算法的时间复杂度是O(n^2)。 就是通过一次次的比较,将当前比较队列中的最小元素推到队首。 对对象进行遍历操作时,会按照下面的顺序进行:

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。

1、下面是最初始的冒泡排序算法

//交换数组中两个元素的位置
function swap (arr, i, j) {
    [arr[i], arr[j]] = [arr[j], arr[i]]
}
function bubbleSort (arr) {
    const len = arr.length;
    for (let i = 0; i < len - 1; i++) {
        for (let j = i + 1; j < len; j++) {
            if (arr[i] > arr[j]) {
                swap(arr, i, j);
            }
        }
    }
}

2、下面是正宗的冒泡排序算法

function bubbleSort2 (arr) {
    const len = arr.length;
    let i,j;
    for (i = 1; i < len; i++) {
        for (j = len-1; j >= i; j--) {
            //把最小的往数组前面排
            if (arr[j - 1] > arr[j]) {
                swap(arr, j - 1, j);
            }
        }
    }
}

3、下面是优化后的冒泡排序算法

function bubbleSort3 (arr) {
    const len = arr.length;
    let flag = true;
    for (let i = 1; i < len&& flag; i++) {
        flag = false;
        for (let j = len-1; j >= i ; j--) {
            if (arr[j - 1] > arr[j]) {
                //交换两者之间的位置
                swap(arr, j - 1, j);
                flag = true;
            }
        }
        console.log(arr);
    }
}
//测试
const arr=[5, 1, 3, 2, 4];
bubbleSort3(arr);
//输出结果为
[ 1, 5, 2, 3, 4 ] //
[ 1, 2, 5, 3, 4 ] //
[ 1, 2, 3, 5, 4 ] //[5, 3, 4] 选出最小元素3,通过逐个交换位置放到第1位
[ 1, 2, 3, 4, 5 ]  //[5, 4] 选出最小元素4,通过逐个交换位置放到第1位

输出结果分析

  • i=0时,从[5,1, 2, 3, 4] 中选出最小元素1,通过逐个交换位置放到第i位。
  • i=1时,从[5, 2, 3, 4] 中选出最小元素2,通过逐个交换位置放到第i位。
  • i=2时,从[5, 3, 4] 选出最小元素3,通过逐个交换位置放到第i位。
  • i=3时,从 [5, 4] 选出最小元素4,通过逐个交换位置放到第i位。

2、选择排序(Selection Sort)

  通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n) 个记录交换之。简单选择排序的时间复杂度也是O(n^2)
通俗的将就是按从左到右的顺序指定一个位置放置(假定位置为i)最小的元素,然后从i~n中选出最小的元素,跟i位置的元素交换,然后i逐渐加一,这样不断循环下去,直到循环到队尾。

function selectSort (arr) {
    const len = arr.length;
    let min,i,j;
    //i是每次循环后放置最小元素的位置
    for (i = 0; i < len - 1; i++) {
        min = i;
        //找到i到len-1之间最小的元素
        for (j = i + 1; j < len; j++) {
            if (arr[min] > arr[j]) {
                min = j;
            }
        }
        if (i != min) {
            //将i和min的位置进行交换
            swap(arr, i, min)
        }
        console.log(arr);
    }
}
//测试
const arr=[5, 1, 3, 2, 4];
selectSort(arr);
[ 1, 5, 3, 2, 4 ] 
[ 1, 2, 3, 5, 4 ] 
[ 1, 2, 3, 5, 4 ]
[ 1, 2, 3, 4, 5 ] 

输出结果分析:

  • i=0时,从[5,1, 2, 3, 4] 中选出最小元素1,然后将最小元素1和首位元素5交换位置。
  • i=1时,从[5,3, 2, 4] 中选出最小元素2,将最小元素2和首位元素5交换位置。
  • i=2时,从[3,5, 4] 中选出最小元素3,此时不用交换位置。
  • i=3时,从[5, 4] 中选出最小元素4,将最小元素4和首位元素5交换位置。

3、插入排序(Insert Sort)

  插入排序有两个循环。外循环将数组元素挨个向右移动,而内循环则对外循环中选中的元素及它前面的那个元素进行比较。如果外循环中选中的元素比内循环中选中的元素小,那么数组元素会向右移动,为内循环中的这个元素腾出位置,

function insertSort (arr) {
    const len = arr.length;
    let temp, i, j;
    for (i = 1; i < len; i++) {
        //临时存放要被比较的元素
        temp = arr[i];
        //判断左边元素是否比右边元素大,大的话就交换位置
        for (j = i; j > 0&& arr[j - 1] > temp ; j--) {
           arr[j] = arr[j - 1];
        }
        //将临时被比较的元素放到最后被比较的元素的位置
        arr[j] = temp;
        console.log(arr);
    }
    return arr;
}
//测试
const arr=[5, 1, 3, 2, 4];
insertSort(arr);
//输出结果
[ 1, 5, 3, 2, 4 ]
[ 1, 3, 5, 2, 4 ]
[ 1, 2, 3, 5, 4 ]
[ 1, 2, 3, 4, 5 ]

输出结果分析:

  • j=i=1时,目标元素是1,将其与其左边的元素比较。1<5,所以此时5直接插入到其右边位置,此时j自减1等于0,内循环结束。然后将目标元素插入到索引为j的位置。输出[ 1, 5, 3, 2, 4 ]
  • j=i=2时,目标元素是3,将其与其左边的元素比较。3<5,所以此时5插入到其右边位置,j自减1等于1,将其与其左边的元素再比较,此时1小于3,内循环结束,然后将位置目标元素插入到索引为j的位置。 输出[1, 3, 5, 2, 4]
  • j=i=3时,目标元素为2,将其与其左边的元素比较。2<5,所以此时将元素5直接插入到其右边位置,j自减1此时左边元素3还是比2大,所以此时将元素3直接插入其右边位置。j自减1等于1,此时1<2,内循环结束。然后将目标元素插入到索引为j的位置。 输出[ 1, 2, 3, 5, 4 ]
  • j=i=4时,目标元素是4,将其与其左边的元素比较。4<5,所以此时将元素5直接插入到其右边位置,j自减1此时4大于3,内循环结束,然后将位置目标元素插入到索引为j的位置。 输出[1, 2,3, 4, 5]

2、高级排序算法

这几种排序方法的时间复杂度都是O(nlogn)

1、希尔排序

  工作原理:通过定义一个间隔来表示排序过程中进行比较的元素之间有多远的间隔,然后比较这两个元素的大小进行交换排序,使得整个数据基本有序。最后间隔等于1时再进行交换排序。其时间复杂度为O(n^3/2)

function shellSort (arr) {
    const len = arr.length;
    let i, j, h = len;
    while (h > 1) {
        //设置间隔
        h = parseInt(h / 3) + 1;
        //设置外层循环起点
        for (i = h; i < len; i++) {
            //将外层循环地点设置为内层循环终点,然后与左边相距间隔的元素进行比较,同时j要大于等于间隔
            for (j = i; j >= h && arr[j] < arr[j - h]; j -= h) {
                swap(arr, j, j - h);
            }
            console.log(arr)
        }
    }
}
//测试
const arr= [61, 85, 19, 88, 68, 8, 70, 29]
shellSort(arr1);

2、归并排序

  把一系列排好的子序列合并成一个大的完整有序序列。将原始数组切分成较小的数组,直到每个小数组只有一项,然后在将小数组归并为排好序的较大数组,直到最后得到一个排好序的最大数组。   归并算法不需要两两比较,不存在跳跃,是一种比较稳定的排序算法。其时间复杂度是O(nlogn)

function mergeSort (arr) {
    const len = arr.length;
    let left, right, step = 1;//当step=1时,是将数组划分为只有一个元素的n个数组,为2、4...时开始合并
    while (step < len) {
        left = 0, right = step;
        //判断是否出界
        while (right + step < len) {
            mergeArray(arr, left, left + step, right, right + step, step);
            //重新设置初始值
            left = right + step;
            right = left + step;
        }
        //为了合并剩下没有被合并的
        if (right < len) {
            mergeArray(arr, left, left + step, right, len, step)
        }
        step *= 2;
    }
    return arr;
}
function mergeArray (arr, startLeft, stopLeft, startRight, stopRight, step) {
    const leftArr = new Array(stopLeft - startLeft + 1);
    const rightArr = new Array(stopRight - startRight + 1);
    //构建左子数组
    let k = startLeft;
    for (let i = 0; i < leftArr.length - 1; i++) {
        leftArr[i] = arr[k++]
    }
    //构建右子数组
    k = startRight;
    for (let i = 0; i < rightArr.length - 1; i++) {
        rightArr[i] = arr[k++];
    }
    leftArr[leftArr.length - 1] = Infinity;
    rightArr[rightArr.length - 1] = Infinity;
    let l = r = 0;
    console.log("left array - ", leftArr);
    console.log("right array - ", rightArr);
    //根据左右两边子数组数据的大小,对数组重新排序
    for (let p = startLeft; p < stopRight; p++) {
        if (leftArr[l] < rightArr[r]) {
            arr[p] = leftArr[l++];
        } else {
            arr[p] = rightArr[r++]
        }
    }
    console.log("arr - ", arr);
}
var nums = [6, 10, 9, 1, 4, 8, 2, 7, 3, 5];
mergeSort(nums);

3、快速排序(Quick Sort)

  基本思想就是通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,已达到整个序列有序的目的。它是处理大数据最快的排序算法之一。
这个算法首先要在列表中选择一个元素作为基准值(pivot)。数据排序围绕基准值进行, 将列表中小于基准值的元素移到数组的底部,将大于基准值的元素移到数组的顶部。时间复杂度为nlogn

  尾递归: 当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归就是尾递归。

function quickSort (arr) {
    var len = arr.length;
    if (len == 0) return [];
    var leftArr = [];
    var rightArr = [];
    //选取基准值
    var pivot = arr[0];
    for (var i = 1; i < len; i++) {
        arr[i] < pivot ? leftArr.push(arr[i]) : rightArr.push(arr[i])
    }
    return [...quickSort(leftArr), pivot, ...quickSort(rightArr)]
}

4、堆排序(Heap Sort)

  是对简单选择排序的一种改进。
堆是一棵完全二叉树。具有以下性质:

  • 每个结点的值都大于或等于其左右结点的值,称为大顶堆
  • 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

  基本思想就是将待排序的序列构造成一个大顶堆。此时,整个序列的最大值,就是顶推的根结点。将它移走,然后将剩余的 n-1 个序列重新构成一个堆,这样就回得到 n 个元素中的次大值。如此反复执行,便能得到一个有序序列了。
堆排序的时间复杂度为O(nlogn)。因为比较和交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。由于构建堆所需的比较次数较多,因此不适合排序序列个数较少的情况。

function heapSort (arr) {
    const len = arr.length
    function sort () {
        //构建堆数据
        buildHeap(arr);
        console.log("构建的堆数据" + arr)
        for (let i = len - 1; i >= 0; i--) {
            swap(arr, 0, i);
            adjustHeap(arr, 0, i);
        }
        console.log("堆排序之后的数据" + arr)
        return arr;
    }
    function buildHeap () {
        const iParent = Math.floor(len / 2) - 1;//获取最大父节点索引
        for (let i = iParent; i >= 0; i--) {
            adjustHeap(arr, i, len)
        }
    }

    /**
     * 堆调整,将堆的末端子节点作调整,使得子节点永远小于父节点,本质上是在比较父子结点的大小并调整位置
     * @param {Array} arr 
     * @param {Number} i 
     */
    function adjustHeap (arr, i, len) {
        //因为数组是从0开始排序的所以
        const left = 2 * i + 1;//左子结点下标
        const right = 2 * i + 2; //右子结点下标
        let largest = i;
        //判断左结点是否超出并判断左子结点和父结点的大小
        left < len && arr[left] > arr[largest] && (largest = left);
        //判断右结点是否超出,并判断右子结点和父结点的大小
        right < len && arr[right] > arr[largest] && (largest = right)
        if (largest != i) {
            swap(arr, i, largest);
            adjustHeap(arr, largest, len);//递归操作,防止排序后的父结点比子结点又小了
        }
    }
    return sort(arr);
}

3、其他排序方法

这三种排序方法的时间复杂度都是O(n)

1、计数排序

  计数排序是一种非比较的排序。时间复杂度为O(n)。这是一种牺牲空间换时间的做法。这种排序方法只适合元素个数有限的数组。若数据范围 k 比要排序的数据 n 大很多的话,就不太适合计数排序。

方法1 利用数组的有序性


function countSort (array) {
    const len = array.length;
    let result = [], countArr = [];
    for (let i = 0; i < len; i++) {
        //计算数组中每个数据的个数
        countArr[array[i]] = countArr[array[i]] ? countArr[array[i]] + 1 : 1;
    }
    countArr.forEach((item, index) => {
        if(item) {
            while(index){
                result.push(index--);
            }
        }
    });
    console.log('countArr :', result);
    return result;
};
const arr = [23, 14, 12, 24, 53, 31, 53, 35, 46, 12, 62, 23]
countSort(arr);

方法2 利用对象的特性

遍历对象时,有以下几个特性

1、首先遍历所有数值键,按照数值升序排列。
2、其次遍历所有字符串键,按照加入时间升序排列。
3、最后遍历所有 Symbol 键,按照加入时间升序排列。

function countSort (arr) {
    const obj = {};
    const result = []
    //遍历原数组,给对象新增键值对,如果已经存在就对应的属性值++,如果不存在则新增键值对
    for (let i = 0; i < arr.length; i++) {
        obj[arr[i]] ? obj[arr[i]]++ :(obj[arr[i]]=1);
    }
    //遍历对象属性名
    Object.keys(obj).forEach(key => {
        while (obj[key]) {
            result.push(key);
            obj[key]--;
        }
    })
    console.log(result);
    return result;
}
const arr = [23, 14, 12, 24, 53, 31, 53, 35, 46, 12, 62, 23]
countSort(arr);

2、基数排序

  基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。用一个二维数组或对象来存储数据,如若是二维数组则用位数做索引来存储同一位数的数据,实际上相当于做了一个大致的排序。一般情况下是从低位到高位进行分割排序。

function radixSort (arr, max) {
    const len = arr.length;
    let unit = 10;
    let base = 1;
    let buckets = [];
    let index;
    //数组中的元素按照个位树、十位数。。的大小进行依次进行排序进行排序。
    //并用二维数组存起来
    for (let i = 0; i < max; i++, unit *= 10, base *= 10) {
        for (let j = 0; j < len; j++) {
            //取整,//依次过滤出个位,十位等等数字
            index = ~~(arr[j] % unit / base)
            !buckets[index] && (buckets[index] = [])
            //往不同桶里添加数据
            buckets[index].push(arr[j])
        }
        let pos = 0, value;
        const bucketCount = buckets.length
        for (let k = 0; k < bucketCount; k++) {
            if (buckets[k] && buckets[k].length) {
                while (value = buckets[k].shift()) {
                    arr[pos++] = value//将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
                }
            }
        }
    }
    return arr;
}
const array = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
const newArr = radixSort(array, 2)

3、桶排序

  桶排序是计数排序的升级版。也是一种非比较排序。原理是将数组分到有限数量的桶里。对每个桶里的数据进行排序,桶内排好序之后,再把每个桶里的数据按照顺序依次取出,组成的数组就是有序的了。也是一种空间换时间的算法。

  为了使桶排序更加高效,我们需要做到这两点:

  • 在额外空间充足的情况下,尽量增大桶的数量。
  • 使用的映射函数能够将输入的 n 个数据均匀的分配到 k 个桶中。

  桶排序的核心:就在于怎么把元素平均分配到每个桶里,合理的分配将大大提高排序的效率。

  桶排序的时间复杂度的计算。 如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k =n / m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k = n / m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)

function bucketSort (array, bucketSize = 5) {
    const len = array.length;
    if (!len) return [];
    if (len <= bucketSize) return quickSort(arr);
    let minValue = array[0];
    let maxValue = array[0];
    const result = [];
    for (let i = 1; i < len; i++) {
        if (array[i] < minValue) {
            minValue = array[i]; //输入数据的最小值
        } else if (array[i] > maxValue) {
            maxValue = array[i]; //输入数据的最大值
        }
    }
    //桶的个数
    let bucketCount = Math.ceil((maxValue - minValue) / bucketSize);
    const buckets = [];
    for (let i = 0; i < bucketCount; i++) {
        buckets[i] = [];
    }
    bucketCount = buckets.length;
    //利用映射函数将数据分配到各个桶中,按从小到大的顺序分配到各个桶中
    for (i = 0; i < len; i++) {
        let index = Math.floor((array[i] - minValue) / bucketSize);
        index = index >= bucketCount ? bucketCount - 1 : index
        buckets[index].push(array[i]);
    }
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = quickSort(buckets[i]); //对每个桶进行排序,这里使用了快速排序
        for (var j = 0; j < buckets[i].length; j++) {
            result.push(buckets[i][j]);
        }
    }
    console.log(result);
    return result;
};
function swap (arr, i, j) {
    [arr[i], arr[j]] = [arr[j], arr[i]]
}
function quickSort (arr) {
    var len = arr.length;
    if (len == 0) return [];
    var leftArr = [];
    var rightArr = [];
    //选取基准值
    var pivot = arr[0];
    for (var i = 1; i < len; i++) {
        arr[i] < pivot ? leftArr.push(arr[i]) : rightArr.push(arr[i])
    }
    return [...quickSort(leftArr), pivot, ...quickSort(rightArr)]
}
const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
bucketSort(array);

参考文献

1、图文详解Heap Sort堆排序算法及JavaScript的代码实现
2、js实现堆排序
3、大话数据结构
4、数据结构与算法JavaScript描述
5、JavaScript 数据结构与算法之美 - 桶排序、计数排序、基数排序