排序算法(js)

83 阅读8分钟

排序分类

  1. 比较排序

这类排序算法通过比较元素之间的大小来决定它们的相对顺序。

常见的比较排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序等。

  1. 非比较排序

与比较排序相反,非比较排序算法不通过比较元素的大小来确定它们的顺序,而是利用元素本身的特性来进行排序。

典型的非比较排序算法有计数排序、桶排序和基数排序。

  1. 稳定排序和不稳定排序

稳定排序算法保持相等元素之间的相对顺序不变,而不稳定排序算法则可能改变相等元素的相对顺序。

例如,冒泡排序和插入排序是稳定的,而快速排序是不稳定的。

一、冒泡排序

实现思路:通过重复遍历要排序的列表,依次比较相邻的元素,如果顺序不对则交换它们。

  • 简单但效率较低,时间复杂度为 O(n^2)。
  • 稳定排序算法。
  • 原地排序,空间复杂度为 O(1)。

方法一:基本逻辑实现

相邻元素两两比较,当一个元素大于右侧相邻元素时,交换它们的位置,当一个元素小于或等于右侧相邻元素时,位置不变。

function bubbleSort(arr) { 
    for (let i = 0; i < arr.length - 1; i++) { 
    
        for (let j = 0; j < arr.length - i - 1; j++) { 
        
            if (arr[j] > arr[j + 1]) { 
                // 利用数组解构赋值 交换j和j+1 
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] 
            } 
        } 
    } 
}

方法二:有序标记实现

这是对方法一的优化版,增加有序标记,如果某一轮比较,一个元素都没有交换就代表整个数组已经有序。

function bubbleSort(arr) { 
    for (let i = 0; i < arr.length - 1; i++) { 
        // 有序标记,每一轮的初始值都是true 
        let isSorted = true; 
        
        for (let j = 0; j < arr.length - i - 1; j++) { 
            if (arr[j] > arr[j + 1]) { 
                // 利用数组解构赋值 交换j和j+1 
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] 
                // 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false 
                isSorted = false; 
            } 
        } 
        
        if (isSorted) { 
            break; 
        } 
    } 
}

方法三:添加边界实现

这是对方法二的进一步优化,优化每一轮比较的次数。增加一个border代表无序边界,然后把最后一次发生元素交换的位置点记下来,下一轮比较就只需要比到这儿为止。

function bubbleSort(arr) { 
    // 记录最后一次交换的边界 
    let lastExchangeIndex = 0; 
    // 无序数组的边界,每次比较只需要比到这里为止 
    let sortBorder = arr.length - 1; 
    
    for (let i = 0; i < arr.length - 1; i++) { 
        // 有序标记,每一轮的初始值都是true 
        let isSorted = true; 
        
        for (let j = 0; j < sortBorder; j++) { 
            if (arr[j] > arr[j + 1]) { 
                // 利用数组解构赋值 交换j和j+1 
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] 
                // 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false 
                isSorted = false; 
                // 更新为最后一次交换元素的位置 
                lastExchangeIndex = j; 
            } 
        } 
        
        sortBorder = lastExchangeIndex; 
        
        if (isSorted) { 
            break; 
        } 
    } 
}

方法四:鸡尾酒排序实现

鸡尾酒排序算法是改进的冒泡排序,鸡尾酒是双向排序。就像排钟从左摆到右后,立即从当前位置摆回去。

function bubbleSort(arr) { 

    for (let i = 0; i < arr.length / 2; i++) { 
        // 有序标记,每一轮的初始值都是true 
        let isSorted = true; 
        
        for (let j = i; j < arr.length - i - 1; j++) { 
            if (arr[j] > arr[j + 1]) { 
                // 利用数组解构赋值 交换j和j+1 
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] 
                // 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false 
                isSorted = false; 
            } 
        } 
        
        if (isSorted) { 
            break; 
        } 
        
        // 顺摆轮结束之前,把isSorted重新标记为true,下面就开始逆摆 
        isSorted = true; 
        
        for (let j = arr.length - i - 1; j > i; j--) { 
            if (arr[j] < arr[j - 1]) { 
                // 利用数组解构赋值 交换j和j+1 
                [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]] 
                // 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false 
                isSorted = false; 
            } 
        } 
        
        if (isSorted) { 
            break; 
        } 
    } 
}

二、选择排序

实现思路

找到数组中的最小(大)元素,将其放到已排序序列的末尾,然后从剩余未排序元素中继续寻找最小(大)元素。

  • 简单但效率较低,时间复杂度为 O(n^2)。
  • 不稳定排序算法。
  • 原地排序,空间复杂度为 O(1)。

方法一:

function selectionSort(arr) { 

    for (let i = 0; i < arr.length - 1; i++) { 
    
        let minIndex = i;  // minIndex用来记录最小值的下标 
        
        for (let j = i + 1; j < arr.length; j++) { 
            if (arr[j] < arr[minIndex]) { 
                minIndex = j; 
            } 
        } 
        
        // 当循环结束,i和minIndex不相等就说明有最小元素,需要交换 
        if (i !== minIndex) { 
            arr[i, minIndex] = arr[minIndex, i] 
        } 
    } 
}

三、插入排序

实现思路:

将数据分为有序区和无序区,每次从无序区中取出一个元素,然后和有序区从后往前挨个比较,比该元素大的往后移。直到找到比该元素小的元素。那么这个元素就插入到他的后面就行。

  • 对于小型数据集或部分有序数据表现良好,平均时间复杂度为 O(n^2)。
  • 稳定排序算法。
  • 原地排序,空间复杂度为 O(1)。

方法一:

function insertionSort(arr) {

    for (let i = 1; i < arr.length; i++) {
    
        for (let j = i; j > 0 && arr[j - 1] > arr[j]; j--) {
            [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]; // 利用数组解构赋值交换元素
        }
        
    }

    return arr;
}

四、快速排序

实现思路

通过选定一个基准值,将数组分为两部分,左边的元素小于基准值,右边的元素大于基准值,然后递归地对左右子数组进行排序。

  • 高效的排序算法,平均时间复杂度为 O(n log n)。
  • 不稳定排序算法。
  • 原地排序,空间复杂度为 O(log n)。

裂缝为观测对象(Hoare)把数组分成两部分:小堆,大堆

需要格外空间的快速排序算法

function crackTheBeastQuickSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    
    // 选择随机基准元素
    const pivot = arr[Math.floor(Math.random() * arr.length)]; 
    const less = [];
    const equal = [];
    const greater = [];

    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < pivot) {
            less.push(arr[i]);
        } else if (arr[i] === pivot) {
            equal.push(arr[i]);
        } else {
            greater.push(arr[i]);
        }
    }

    return [...crackTheBeastQuickSort(less), ...equal, ...crackTheBeastQuickSort(greater)];
}

原地排序算法

function partition(arr, low, high) {
    const pivot = arr[high];
    let i = low - 1;
    
    for (let j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            [arr[i], arr[j]] = [arr[j], arr[i]];
        }
    }
    
    [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
    
    return i + 1;
}

function quickSortInPlace(arr, low, high) {

    if (low < high) {
        const partitionIndex = partition(arr, low, high);
        
        quickSortInPlace(arr, low, partitionIndex - 1);
        quickSortInPlace(arr, partitionIndex + 1, high);
    }
    
    return arr;
}

五、归并排序

实现思路

将数组分成两个子数组,分别排序,然后合并两个已排序的子数组。

  • 稳定的排序算法,时间复杂度为 O(n log n)。
  • 需要额外的空间来存储中间结果,空间复杂度为 O(n)。

方法一:

function mergeSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }

    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);

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

        while (leftIndex < left.length && rightIndex < right.length) {
        
            if (left[leftIndex] < right[rightIndex]) {
                result.push(left[leftIndex]);
                leftIndex++;
            } else {
                result.push(right[rightIndex]);
                rightIndex++;
            }
        }

        return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
    };

    return merge(mergeSort(left), mergeSort(right));
}

六、堆排序

实现思路

在堆排序中,首先将无序数组构建成最大堆,然后循环删除堆顶元素,将其移到末尾,并调整产生新的堆顶,重复这个过程直到整个数组有序。

  • 高效的排序算法,时间复杂度为 O(n log n)。
  • 不稳定排序算法。
  • 原地排序,空间复杂度为 O(1)。

方法一:

function heapSort(arr) {

  // 把无序数组构建成最大堆
  for (let i = (arr.length - 2) / 2; i >= 0; i--) {
    downAdjust(arr, i, arr.length);
  }
  
  // 循环删除堆顶元素,移到末尾,调整产生新的堆顶
  for (let i = arr.length - 1; i > 0; i--) {
    [arr[i], arr[0]] = [arr[0], arr[i]];
    downAdjust(arr, 0, i);
  }

  return arr;
}

/**
 *
 * @param {待调整的堆} arr
 * @param {要下沉的父节点} parentIndex
 * @param {堆的有效大小} length
 */
function downAdjust(arr, parentIndex, length) {

  // temp保存父节点值,用户最后的赋值
  let temp = arr[parentIndex];
  let childIndex = 2 * parentIndex + 1;
  
  while (childIndex < length) {
  
    // 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
    if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {
      childIndex++;
    }
    
    // 如果父节点大于任何一个孩子的值,则直接跳出
    if (temp >= arr[childIndex]) {
      break;
    }
    
    // 无须真正交换,单向赋值即可
    arr[parentIndex] = arr[childIndex];
    parentIndex = childIndex;
    childIndex = 2 * childIndex + 1;
  }
  
  arr[parentIndex] = temp;
}

七、计数排序

实现思路

统计数组中每个元素出现的次数,根据元素的计数信息将元素放回正确的位置。

  • 非比较排序算法,适用于特定范围的整数排序,时间复杂度为 O(n + k)(k 为整数范围)。
  • 稳定排序算法。
  • 需要额外空间,空间复杂度取决于整数范围。

方法一:

function heapSort(arr) {

  // 把无序数组构建成最大堆
  for (let i = (arr.length - 2) / 2; i >= 0; i--) {
    downAdjust(arr, i, arr.length);
  }
  
  // 循环删除堆顶元素,移到末尾,调整产生新的堆顶
  for (let i = arr.length - 1; i > 0; i--) {
    [arr[i], arr[0]] = [arr[0], arr[i]];
    downAdjust(arr, 0, i);
  }

  return arr;
}

/**
 *
 * @param {待调整的堆} arr
 * @param {要下沉的父节点} parentIndex
 * @param {堆的有效大小} length
 */
function downAdjust(arr, parentIndex, length) {

  // temp保存父节点值,用户最后的赋值
  let temp = arr[parentIndex];
  let childIndex = 2 * parentIndex + 1;
  
  while (childIndex < length) {
  
    // 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
    if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {
      childIndex++;
    }
    
    // 如果父节点大于任何一个孩子的值,则直接跳出
    if (temp >= arr[childIndex]) {
      break;
    }
    
    // 无须真正交换,单向赋值即可
    arr[parentIndex] = arr[childIndex];
    parentIndex = childIndex;
    childIndex = 2 * childIndex + 1;
  }
  
  arr[parentIndex] = temp;
}