面试秘籍之排序算法

一、冒泡排序

排序思想:列表每两个相邻的数进行比较,如果前面的数比后面的数大,则交换这两个数,一轮排序完成后,则无序区减少一个数,有序区增加一个数。

function sort(list) {
    for (let i= 0; len = list.length-1; i list[j+1]) {
                // 交换两个数
                const m = list[j];
                list[j] = list[j+1];
                list[j+1] = m;
                exchange = true;
            }
        }
        // 如果在一轮冒泡过程中没有交换过,说明此时的列表已经是排序好的了,直接结束循环
        if (!exchange) {
            return;
        }
    }
}

二、选择排序

排序思想:刚开始将整个数组看作一个无序区,每一轮拿无序区的第一个数与无序区其他数值依次进行比较,遇到更小的数则交换,每一轮排序完成后有序区增加一个数,无序区减少一个数。

function sort (list) {
    for (let i=0,len=list.length; i list[j]) {
                // 交换两个数
                const m = list[i];
                list[i] = list[j];
                list[j] = m;
            }
        }
    }
}

三、插入排序

排序思想:刚开始将整个数组看作一个无序区,每一轮拿无序区的第一数与有序区的数从后往前依次进行比较,遇到更大的数则交换,每一轮排序完成后,有序区增加一个数,无序区减少一个数。

function sort(list) {
    for (let i=1,len=list.length; i=0; j--) {
            if (list[j+1] < list[j]) {
                // 交换两个数
                const m = list[j+1];
                list[j+1] = list[j];
                list[j] = m;
            }
        }
    }
}

四、快速排序

排序思想:取一个元素p(第一个数),使元素p归位,此时列表会被p分成两部分,左边都比p小,右边都比p大,然后左边和右边再分别进行递归使元素归位,最终完成排序。

function sort(list, left, right) {
    if (left < right) {
        const mid = homing(list, left, right);
        sort(list, left, mid-1);
        sort(list, mid+1, right);
    }
}

/**
* 归位函数
* 首先将第一个元素缓存起来,左指针刚开始指向第一个元素
* 然后右指针依次往左走,直到找到比缓存起来的元素小的元素,将该元素赋值给当前左指针指向的元素
* 然后左指针依次往右走,直到找到比缓存起来的元素大的元素,将该元素赋值给当前右指针指向的元素
* 重复以上操作,当左指针和右指针重合时,便找到了要归位的地方,将缓存起来的元素赋值给左指针指向的元素,完成归位
*/
function homing(li, left, right) {
    const tmp = li[left];
    while (left < right) {
        while (left < right && li[right] >= tmp) {
            right--;
        }
        li[left] = li[right];
        while(left < right && li[left] <= tmp) {
            left++;
        }
        li[right] = li[left];
    }
    li[left] = tmp;
    return left;
}

五、归并排序

排序思想:假设有两个排序好的数组,如何将他们合并成一个排序好的数组,首先创建一个空的新数组,将两个有序的数组依次进行比较,每次将较小的数放到新数组中,最终得到一个排序好的新数组,此过程称为归并。数组为空或者只有一个元素时就是有序的,就可以进行归并,归并排序就是先两两进行归并,再利用递归,将归并后的两个数组再进行归并,最终得到排序好的数组。

function sort(list) {
    if (list && list.length > 1) {
        const mid = Math.floor(list.length / 2);
        const left = list.slice(0, mid);
        const right = list.slice(mid);
        return merge(sort(left), sort(right));
    }
    return list;
}

/**
* 归并函数
* 首先创建一个空的新数组
* 将两个有序数组中的元素依次进行比较,每次将较小的数放到新数组中
* 最终得到一个排序好的新数组
*/
function merge(leftList, rightList) {
    const newList = [];
    const leftLength = leftList && leftList.length;
    const rightLength = rightList && rightList.length;
    let i = 0;
    let j = 0;
    while (i < leftLength && j < rightLength) {
        if (leftList[i] < rightList[j]) {
            newList.push(leftList[i++]);
        } else {
            newList.push(rightList[j++]);
        }
    }
    while (i < leftLength) {
        newList.push(leftList[i++]);
    }
    while (j < rightLength) {
        newList.push(rightList[j++]);
    }
    return newList;
}

六、堆排序

排序思想:二叉树的存存储方式分为链式存储和顺序存储,这里堆排序使用顺序存储。堆是一种特殊的完全二叉树,堆分为大根堆和小根堆,满足任一节点都比其孩子节点大的一个完全二叉树就是大根堆,满足任一节点都比其孩子节点小的一个完全二叉树就是小根堆,这里堆排序使用大根堆。假设根节点的左右子树都是堆,但是根节点不满足堆的性质,可以通过一次向下调整来将其变成一个堆,这就是堆的向下调整性质。堆排序的排序过程是,首先构造一个大根堆(此时整个堆是无序区),然后将堆顶的元素取出放到有序区(也就是数组的最后),然后将堆的最后一个元素(也就是无序区的最后一个元素)放到堆顶,堆就少了一个元素,此时通过一次向下调整重新使堆有序,调整后的堆顶就是整个数组的第二大元素,然后重复之前的操作依次将元素放到有序区,直到堆变空,便可得到排序好的数组。

function sort(list) {
    if (list && list.length > 1) {
        const len = list.length;
        // 首先构造大根堆,从最后一个不是叶子节点的节点开始遍历,从后往前依次进行向下调整
        for (let i = Math.floor((len-2)/2); i>=0; i--) {
            sift(list, i, len-1);
        }
        // 然后将堆的第一元素与有序区的第一个元素进行交换,此时有序区增加一个,无序区减少一个,再进行一次堆的向下调整,然后重复上述操作,最终使整个数组有序
        for(let i = len-1; i>=0; i--){
            const m = list[0];
            list[0] = list[i];
            list[i] = m;
            sift(list, 0, i-1);
        } 
    }
}

/**
* 堆的向下调整
* 先从根节点开始,如果孩子节点比父节点大,则将该孩子节点赋值给父节点
* 然后指针指向下一层,重复上面的操作,直到找到孩子节点没有比父节点大的节点,终止循环
* 最后将原始的根节点赋值给当前父节点
*/
function sift(li, low, high) {
    const tmp = li[low]; // 缓存根节点
    let i = low; // 当前的父节点,最开始指向根节点
    let j = i*2+1; // 当前的孩子节点,最开始指向根节点的左孩子节点

    while (j <= high) {
        // 如果有右孩子节点且比左孩子节点大,则j指向右孩子节点
        if (j+1 <= high && li[j+1] > li[j]) {
            j++;
        }
        if (li[j] > tmp) {
            li[i] = li[j]; // 将较大的孩子节点赋值给父节点
            i = j; // i指向下一层
            j = i*2 +1;
        } else {
            break; // 如果当前子节点没有比原始根节点大,结束循环
        }
    }

    li[i] = tmp; // 最后将原始的根节点赋值给当前父节点
}

总结

排序算法复杂度总结图.png

更多个人文章

  1. 两个跨域页面进行跳转传参的终极方案
  2. 深入理解Event Loop的运行机制
  3. hashHistory和browserHistory的区别
  4. 面试秘籍之手写系列

你可能感兴趣的:(面试秘籍之排序算法)