稳定性

它只表示两个值相同的元素在排序前后是否有位置变化。如果前后位置变化,则排序算法是不稳定的,否则是稳定的
快排、堆排是不稳定的 合并、基数是稳定的

快排

/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
// 快速排序
function quickSort(array, start = 0, end = array.length - 1) {
if (start < end) {
// 取基准值,将array的每个元素与基准值大小比对后放在基准值的左边或者右边
let pivot = divide(array, start, end);
// 对小于基准值的元素排序
quickSort(array, start, pivot - 1);
// 对大于基准值的元素排序
quickSort(array, pivot + 1, end);
}
return array;
}

function divide(array, start, end) {
// 取中位数作为pivot以避免基本有序时时间复杂度飙升的问题
let mid = (start + end) >> 1; // 即mid = Math.floor((start + end) / 2);
// 将中位数交换到开头
[array[start], array[mid]] = [array[mid], array[start]];
let pivot = start;

// 构建一个[low, high]的滑动窗口,想要实现窗口里包含的都是大于pivot的数
let low = start + 1;
// 一开始窗口里还没有内容(low=high)
for (let high = low; high <= end; high++) {
// 找到的数是小于pivot的数
if (array[high] < array[pivot]) {
// 小于pivot的数丢到窗口的第一位去
[array[low], array[high]] = [array[high], array[low]];
// 将第一位挤出窗口
low++;
}
// 大于pivot的数,则仅high++,使得窗口扩大
}
// 将pivot插回该在的位置
// 区间(pivot, low-1]的数都小于pivot
// 区间[low, high)的数都大于等于pivot
// 故将pivot与low-1交换即可(不与low交换,是因为[low, high)可能没有包含任何的元素,是越界的)
[array[pivot], array[low - 1]] = [array[low - 1], array[pivot]];
return low - 1;
}
// 调用自定义快排函数
return quickSort(nums);
};

归并

function sort(arr, startIndex = 0, endIndex = arr.length - 1) {
// 递归结束条件:startIndex大于等于endIndex的时候
if (startIndex >= endIndex) {
return;
}

// 折半递归
let midIndex = parseInt((startIndex + endIndex) / 2);
sort(arr, startIndex, midIndex);
sort(arr, midIndex + 1, endIndex);
// 将两个有序的小数组,合并成一个大数组
merge(arr, startIndex, midIndex, endIndex);
}

function merge(arr, startIndex, midIndex, endIndex) {
// 新建一个大数组
let tempArr = [];
let p1 = startIndex;
let p2 = midIndex + 1;
let p = 0;

// 比较两个有序小数组的元素,依次放入大数组中
while (p1 <= midIndex && p2 <= endIndex) {
if (arr[p1] <= arr[p2]) {
tempArr[p++] = arr[p1++];
} else {
tempArr[p++] = arr[p2++];
}
}

// 右侧小数组已排序完毕,左侧小数组还有剩余,将左侧小数组元素依次放入大数组尾部
while (p1 <= midIndex) {
tempArr[p++] = arr[p1++];
}
// 左侧小数组已排序完毕,右侧小数组还有剩余,将右侧小数组元素依次放入大数组尾部
while (p2 <= endIndex) {
tempArr[p++] = arr[p2++];
}

for (let i = 0; i < tempArr.length; i++) {
arr[i + startIndex] = tempArr[i];
}
}

堆排序

建立堆顺序 -从下往上

根据大顶堆的性质,每个节点的值都大于或者等于它的左右子节点的值。所以我们需要找到所有包含子节点的节点,也就是非叶子节点,然后调整他们的父子关系,非叶子节点遍历的顺序应该是从下往上,这比从上往下的顺序遍历次数少很多,因为,大顶堆的性质要求父节点的值要大于或者等于子节点的值,如果从上往下遍历,当某个节点即是父节点又是子节点并且它的子节点仍然有子节点的时候,因为子节点还没有遍历到,所以子节点不符合大顶堆性质,当子节点调整后,必然会影响其父节点需要二次调整。但是从下往上的方式不需要考虑父节点,因为当前节点调整完之后,当前节点必然比它的所有子节点都大,所以,只会影响到子节点二次调整。相比之下,从下往上的遍历方式比从上往下的方式少了父节点的二次调整。
(类似自己去堆一堆沙子,肯定是从下面往上更便捷,否则你需要架子架住)

(21条消息) 十大经典排序算法-堆排序算法详解_小小学编程的博客-CSDN博客

最后一个非叶子节点的位置-arr.length / 2 -1

对于一个完全二叉树,在填满的情况下(非叶子节点都有两个子节点),每一层的元素个数是上一层的二倍,根节点数量是1,所以最后一层的节点数量,一定是之前所有层节点总数+1,所以,我们能找到最后一层的第一个节点的索引,即节点总数/2(根节点索引为0),这也就是第一个叶子节点,所以第一个非叶子节点的索引就是第一个叶子结点的索引-1。那么对于填不满的二叉树呢?这个计算方式仍然适用,当我们从上往下,从左往右填充二叉树的过程中,第一个叶子节点,一定是序列长度/2,所以第一个非叶子节点的索引就是arr.length / 2 -1。

(21条消息) 十大经典排序算法-堆排序算法详解_小小学编程的博客-CSDN博客

左孩子节点 -2*i+1

// 下沉调整
// 最大堆
function downAdjust(arr, parentIndex, length) {
// 缓存父节点的值,用于最后进行赋值,而不需要每一步进行交换
let temp = arr[parentIndex];
// 孩子节点下标,暂时定为左孩子节点下标
let childIndex = 2 * parentIndex + 1;

while (childIndex < length) {
// 当存在右孩子节点,且右孩子节点的值小于左孩子节点的值,childIndex记录为右孩子节点的下标
// childIndex实际记录的是最小的孩子节点的下标
if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {
childIndex++;
}

// 如果父节点的值比孩子节点的值都大,则调整结束
if (temp >= arr[childIndex]) {
break;
}

// 将最小的孩子节点的值赋值给父节点
arr[parentIndex] = arr[childIndex];
parentIndex = childIndex;
childIndex = 2 * parentIndex + 1;
}
arr[parentIndex] = temp;
}

// 堆排序
function sort(arr) {
// 把无序数组构建成二叉堆
for (let i = parseInt(arr.length / 2) - 1; i >= 0; i--) {
downAdjust(arr, i, arr.length);
}
console.log(arr);

// 循环删除堆顶元素,移到集合尾部,调节堆产生新的堆顶
for (let i = arr.length - 1; i > 0; i--) {
// 最后一个元素与第一个元素交换
[arr[0], arr[i]] = [arr[i], arr[0]];
downAdjust(arr, 0, i);
}
}