zoukankan      html  css  js  c++  java
  • 前端的几种基本算法(二分查找,选择排序,插入排序,希尔排序,归并排序,快速排序,堆排序)

    现在前端对于算法的要求是越来越高了,以下简单归纳下前端的几种基本的排序算法与二分查找相关的内容

    二分查找

    二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

    在有序的数组中查询一个元素用二分查找法是非常高效的,在应用中可以简单的分为三种情况,即:查找目标值,查找比目标值大的第一个元素,查找比目标值小的第一个元素。

     

    查找目标值

    function binarySearch(arr, target) {
      let l = 0
      let r = arr.length - 1
      let mid = 0
    
      while(l <= r) {
        mid = (l + r) >> 1
        if (arr[mid] > target) {
          r = mid - 1
        } else if (arr[mid] < target) {
          l = mid + 1
        } else {
          return mid
        }
      }
    
      return -1
    }

    查找比目标值大的第一个元素

    function binarySearchFirstGreate(arr, target) {
      let l = 0
      let r = arr.length - 1
      let mid = 0
    
      while(l <= r) {
        mid = (l + r) >> 1
        if (arr[mid] > target) {
          r = mid - 1
        } else {
          l = mid + 1
        }
      }
    
      return l
    }

    查找比目标值小的第一个元素

    function binarySearchFirstLess(arr, target) {
      let l = 0
      let r = arr.length - 1
      let mid = 0
    
      while(l <= r) {
        mid = (l + r) >> 1
        if (arr[mid] < target) {
          l = mid + 1
        } else {
          r = mid - 1
        }
      }
    
      return r
    }

    选择排序

    选择排序的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。

    选择排序是不稳定的排序方法。

    function selectionSort(arr) {
      let l = arr.length
      for(let i = 0; i < l; i++) {
        for(let j = i + 1; j < l; j++) {
          if (arr[i] > arr[j]) {
            [arr[i], arr[j]] = [arr[j], arr[i]]
          }
        }
      }
    }

    插入排序

    插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。

    插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动

    它与选择排序的区别是:

    1. 选择排序是在未排列的数据中选取最大(小)的值。
    2. 插入排序是在已排列的数据中寻找正确的位置,所以插入排序比选择排序性能会好很多。

    function insertSort(arr) {
      let l = arr.length
      for(let i = 1; i < l; i++) {
        for(let j = i; j > 0; j--) {
          if (arr[j - 1] > arr[j]) {
            [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
          }
        }
      }
    }

    希尔排序(增强版的插入排序)

    希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。

    希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

    function shellSort(arr) {
      let t = new Date()
      let len = arr.length
      let h = 1
      while(h < len / 3) h = 3 * h + 1 // 1, 4, 13, 40, 121, 364, 1093
      while(h >= 1) {
        // 将数组变为h有序
        for(let i = h; i < len; i++) {
          for(let j = i; j >= h; j -= h) {
            if (arr[j] < arr[j - h]) {
              [arr[j - h], arr[j]] = [arr[j], arr[j - h]]
            }
          }
        }
        h = Math.floor(h / 3)
      }
    }

    归并排序

    归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

     

    首先归并排序需要一个将两个有序数组合并的方法:

    function merge(a, l, m, r) {
      let i = l, j = m + 1, aux = []
    
      for (let k = l;k <= r; k++) {
        aux[k] = a[k]
      }
    
      for (let k = l; k <= r; k++) {
        if (i > m) {
          a[k] = aux[j++]
        } else if (j > r) {
          a[k] = aux[i++]
        } else if (aux[j] < aux[i]) {
          a[k] = aux[j++]
        } else {
          a[k] = aux[i++]
        }
      }
    
      return a
    }

    归并排序的算法可以分为两种方式:

    1. 自顶向下:采用递归的方式,不断的将分割的子数组,直到将子数组的个数分割成1,然后再用merge合并成一个有序的大数组
    2. 自底向上:采用双层循环的方式,先将数组内的元素与相邻元素归并,然后递增到最后的一个大数组

    自顶向下

    function sort_down(a, l, r) {
      if (l >= r) return
      let m = (l + r) >> 1
      sort_down(a, l, m) // 左边排序
      sort_down(a, m + 1, r) // 右边排序
      if (a[m] > a[m + 1]) {
        merge(a, l, m, r) // 合并
      }
    }

    自底向上

    function sort_up(a) {
      let n = a.length
      for (let i = 1; i < n; i += i) {
        for (let j = 0; j < n - i; j += i + i) {
          merge(a, j, i + j - 1, Math.min(j + i + i - 1, n - 1))
        }
      }
    }

    快速排序

    快速排序(Quicksort)是对冒泡排序算法的一种改进。

    快速排序是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速的原地排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

    快速排序分为两种方式:

    1. 二向切分快速排序:先进行左指针的值与base的比较,如果比base大,则从右指针递减与base的比较,如果遇到比base小的则进行左指针与右指针互换,以此规则循环,直到比base小的都在左,大的都在右。然后进行递归直到整个数组有序。
    2. 三向切分快速排序:设立三个指针:左指针,中指针,右指针。三向切分只比较中指针指向的值与base的大小,如果中指针小于base,则左指针与中指针互换且都递增1,如果比base大,则中指针与右指针互换,继续与base比较,如果相等,则中指针加1,直到整个数组的左部分都比base小,右部分都比base大,然后递归直到整个数组有序。(左指针的索引是与base最近的,直到右指针的索引靠近base,则该循环结束。)

    三向切分比二向切分的优化点在于:如果数组能有重复值的话,三向切分不需要重复比较,而二向切分是要重复比较的,对于大批量的用户数据排序,该特性非常有用。

     三向切分图

    二向切分快速排序

    function quickSort(arr, l, r) {
      if (l >= r) return
    
      let base = arr[l]
      let i = l
      let j = r
    
      while(l <= r) {
        while(l < r && arr[++l] < base) {}
        while(l < r && arr[--r] > base) {}
        
        if (l < r) {
          [arr[l], arr[r]] = [arr[r], arr[l]]
        } else {
          [arr[l - 1], arr[i]] = [base, arr[l - 1]]
          break
        }
      }
    
      quicksort(arr, i, l - 2)
      quicksort(arr, l, j)
    }

    三向切分快速排序

    function sQuickSort(arr, l, r) {
      if (l >= r) return
      let lf = l
      let ri = r
      let v = arr[lf]
      let i = l + 1
    
      while(i <= ri) {
        if (v > arr[i]) {
          [arr[lf++], arr[i++]] = [arr[i], arr[lf]]
        } else if (v < arr[i]) {
          [arr[i], arr[ri--]] = [arr[ri], arr[i]]
        } else {
          i++
        }
      }
      squicksort(arr, l, lf - 1)
      squicksort(arr, ri + 1, r)
    }

    堆排序

    堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

     堆有两个重要的基本操作,即在堆有序时对单个元素的下沉和上浮操作。

    以大顶堆为例(大顶堆即堆顶元素为最大,小顶堆的堆顶元素为最小):

    大顶堆的下沉:

    function sink(arr, k, len) {
      while(2 * k + 1 < len) {
        let j = 2 * k + 1
        if (j < len - 1 && arr[j] < arr[j + 1]) j++
        
        if (arr[k] >= arr[j]) break
        
        [arr[k], arr[j]] = [arr[j], arr[k]]
        k = j
      }
    }

    大顶堆的上浮:

    function swim(arr, len) {
      let p = 0 // 父级节点
      while(len > 0) {
        p = (len - 1) >> 1
    
        // (len & 1) 为0的情况下是有兄弟节点的,选出最大的与父级节点比较
        if ((len & 1) === 0 && arr[len] < arr[len - 1]) len--
    
        if (arr[len] <= arr[p]) break
    
        [arr[len], arr[p]] = [arr[p], arr[len]]
        len = p
      }
    }

    在有序堆中每次添加和删除元素后执行的下沉和上浮操作,都会得到目前有序堆中的最大(小)元素,以此特性就可以进行对元素排序。

    function heapSort(arr) {
      let len = arr.length
      let copy = []
      // 建立一个有序的堆
      for (let i = (len - 1) >> 1; i >= 0; i--) {
        sink(arr, i, len)
      }
      // 每次将堆顶元素与堆尾元素进行替换,再进行堆顶元素的下沉且堆长度减一,以此可以得到一个有序的数据
      while(len--) {
        [arr[0], arr[len]] = [arr[len], arr[0]]
        sink(arr, 0, len)
      }
    }

    大顶堆的的排序得到的是一个降序排序,小顶堆的则得到的是升序数据。

    简单描述各排序算法的性能特点:

    算法 是否稳定 是否原地排序 时间复杂度 空间复杂度 备注
    选择排序 N2 1 取决于输入元素的排列情况
    插入排序 介于N和N2之间 1
    希尔排序 NlogN?
    N6/5?
    1
    快速排序 NlogN lgN 运行效率由概率提供保证
    三向快速排序 介于N和NlogN之间 lgN 运行效率由概率保证,同时也
    取决于输入元素的分布情况
    归并排序

    NlogN

    N  
    堆排序 NlogN 1  
    每一次的记录,都是向前迈进的一步
  • 相关阅读:
    [git]git的简单配置使用 (将你的代码上传到Github)
    学习进度报告【第六周】
    [错误解决]SpringMVC接收对象 中文乱码问题解决
    [架构]myeclipse配置SpringMVC 以及简单应用 教程
    [机器学习]AttributeError: module 'tensorflow' has no attribute 'ConfigProto' 报错解决方法
    [机器学习]RuntimeError: The Session graph is empty. Add operations to the graph before calling run(). 报错解决方法
    [python]机器学习 k-mean 聚类分析
    学习进度报告【第五周】
    学习进度报告【第四周】
    unity3d优化总结篇
  • 原文地址:https://www.cnblogs.com/kdcg/p/14648745.html
Copyright © 2011-2022 走看看