常见排序算法:

2016-08-17  本文已影响80人  安静1337

插入排序

(defun insertion-sort (arr symbol)
  "
arr is of type LIST
symbol must be `>` or `<`
                                      执行时间          执行次数
for j = 1 until arr.length            c1               n
    key = arr[j]                      c2               n - 1
    i = j - 1                         c4               n - 1

                                                       n
    while i >= 0 and arr[i] > key     c5               Σ Tj
                                                       j=1
                                                       n
        arr[i + 1] = a[i]             c6               Σ (Tj - 1)
                                                       j=1
                                                       n
        i = i - 1                     c7               Σ (Tj - 1)
                                                       j=1
    a[i + 1] = key                    c8               n - 1
"
  (let ((len (length arr)))
    (do ((j 1 (1+ j)))
    ((>= j len) arr)
      (let ((key (nth j arr)))
    (do ((i (- j 1) (1- i)))
        ((not (and (>= i 0) (funcall symbol (nth i arr) key))) (setf (nth (+ i 1) arr) key))
      (setf (nth (+ i 1) arr) (nth i arr)))))))

    /**
      * insert sort
      *
      * @param arr
      * @return sorted array
      */
    def `insert-sort` (arr: Array[Int]): Array[Int] = {
        for (i <- 0 until arr.length) {
            var j = i
            val k = arr(j)
            while (j > 0 && arr(j) > arr(j - 1)) {
                arr(j) = arr(j - 1)
                arr(j - 1) = k
                j = j - 1
            }
        }
        arr
    }

归并排序

; 分治法思想:
; 将原问题分解为几个规模较小但类似于原问题的子问题
; 递归求解这些子问题
; 最后合并这些子问题的解来简历原问题的解

; 并归排序算法完全遵守分治模式,即
; 分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列
; 解决:使用并归排序递归地排序两个子序列
; 合并:合并两个已排序的子序列以产生已排序的答案

; 而它的关键操作就是“合并”:两个'已排序'序列的合并
; 下面,可以定义一个辅助函数merge(arr, l, m , r)来完成合并

(defun ~merge (arr l m r)
  "
`包CL已经含有merge名称的函数 此处需要换一个名称`
arr(ay) - 数组
l - left 下标
m - mid 下标
r - right 下标

MERGE(array, l, m, r)
    n1 = m - l + 1
    n2 = r - m
    let al[n1] and ar[n2] be new arrays

    for i = 0 until n1
        al[i] = array[l + i]

    for j = 0 until n2
        ar[j] = array[m + j + 1]

    i = 0, j = 0

    for k = l to r
        case i < n1 and j < n2
            if al[i] <= ar[j]
                array[k] = al[i]
                i =  i + 1
            else
                array[k] = ar[j]
                j =  j + 1
        case i < n1
            array[k] = al[i]
            i = i + 1
        case j < n2
            array[k] = ar[j]
            j = j + 1
"
  (let* ((n1 (+ 1 (- m l)))
     (n2 (- r m))
     (n3 (+ n1 n2))
     (al (make-array `(,n1) :initial-element nil))
     (ar (make-array `(,n2) :initial-element nil)))
    (dotimes (i n1)
      (setf (svref al i) (aref arr (+ i l))))
    (dotimes (j n2)
      (setf (svref ar j) (aref arr (+ j m 1))))
    (let ((i 0)
      (j 0))
      (do ((k l (1+ k)))
      ((>= k (+ n3 l)) arr)
    (cond ((and (< i n1) (< j n2))
           (if (<= (aref al i) (aref ar j))
           (progn
             (setf (aref arr k) (aref al i))
             (setf i (+ i 1)))
           (progn
             (setf (aref arr k) (aref ar j))
             (setf j (+ j 1)))))
          ((< i n1)
           (progn
         (setf (aref arr k) (aref al i))
         (setf i (+ i 1))))
          ((< j n2)
           (progn
         (setf (aref arr k) (aref ar j))
         (setf j (+ j 1)))))))))

; 在~merge函数中,3个for循环运行时间都跟数组长度相关
; 其余步骤可用常量c代替
; 所以次函数时间复杂度可以简化成
; c1 * n1 + c2 * n2 + c3 * n3 + c ==> 线性阶 ==> O(n)


(defun merge-sort (arr l r)
  "
归并排序数组A[l ... r]
如果l >= r,则表示子数组最多只有一个元素,可以当作已排列数组序列

MERGE-SORT(arr, l, r)
    if l < r
        m = |(l + r) / 2|
        MERGE-SORT(arr, l, m)
        MERGE-SORT(arr, (m + 1), r)
        MERGE(arr, l, m, r)
"
  (if (< l r)
      (let ((m (floor (+ l r) 2)))
    (merge-sort arr l m)
    (merge-sort arr (+ m 1) r)
    (~merge arr l m r))))
    /**
      * 数组arr以下标m为分界点,[1 - m](m - r],必须有序一致
      * @param arr
      * @param l
      * @param m
      * @param r
      * @return sorted array
      */
    def merge (arr: Array[Int], l: Int, m: Int, r: Int): Array[Int] = {
        val n1 = m - l + 1
        val n2 = r - m
        val left = new Array[Int](n1)
        val right = new Array[Int](n2)

        for (i <- 0 until n1)
            left(i) = arr(l + i)

        for (j <- 0 until n2)
            right(j) = arr(m + 1 + j)

        var i = 0
        var j = 0
        var end = 0
        for (k <- l to r) {
            end match {
                case 1 => {
                    arr(k) = right(j)
                    j = j + 1
                }
                case 2 => {
                    arr(k) = left(i)
                    i = i + 1
                }
                case _ => {
                    if (i > n1 - 1) {
                        arr(k) = right(j)
                        j = j + 1
                        end = 1
                    } else if (j > n2 - 1) {
                        arr(k) = left(i)
                        i = i + 1
                        end = 2
                    } else if (left(i) < right(j)) {
                        arr(k) = left(i)
                        i = i + 1
                    } else {
                        arr(k) = right(j)
                        j = j + 1
                    }
                }
            }
        }
        arr
    }

    def `merge-sort` (arr: Array[Int], l: Int, r: Int): Array[Int] = {
        if (l < r) {
            val m: Int = math.floor((l + r) / 2).toInt
            `merge-sort`(arr, l, m)
            `merge-sort`(arr, m + 1, r)
            merge(arr, l, m, r)
        }
        arr
    }

堆排序

;;;; 6 堆排序

;;;; 6.1 堆

; 如果输入数组中仅有常数个元素需要在排序过程中存储在数组外
; 则称排序算法是原址的(in place)

; 插入排序是一种原址排序
; 而归并排序中, merge过程并不是原址的

; 堆排序是原址的,时间复杂度为O(nlgn),它具有插入排序和归并排序的优点

(defun parent (i)
  "根据节点下标求父节点 i [0 ...]
"
  (if (= i 0)
      0
      (floor (/ (- i 1) 2))))

(defun left (i)
  "i [0 ...]"
  (+ (* 2 i) 1))

(defun right (i)
  "i [0 ...]"
  (* 2 (+ i 1)))

; 二叉堆的两种形势:
; 最大堆:除了根以外的所有节点i都满足 A[parent(i)] >= A[i]
; 最小堆:除了根以外的所有节点i都满足 A[parent(i)] <= A[i]

; 堆排序算法中,一般使用最大堆,最小堆通常用于有限队列

; 如果把堆看成一棵树,如果包含n个元素的堆可以看成一棵完全二叉树
; 那么该堆的高度就是lgn,堆结构上的一些基本操作的运行时间至多与树的高度成正比
; 即时间复杂度为O(lgn)


;;;; 6.2 维护堆的性质

(defun max-heapify (arr i &optional (root-index 0))
  "
用于维护最大堆性质的重要过程
arr - 数组
i - 下标(相对数组, 以起始下标为准的新数组)
root-index - 起始下标(整个数组)

输入数组arr和下标i
调用该函数时,假设父节点left(i)和right(i)的二叉树都是最大堆
但A[i]可能小于其孩子,这就违背了最大堆的性质
MAX-HEAPIFY通过让A[i]的值在最大堆中逐渐降级
从而使得以下标i为根节点的子树重新遵守最大堆的性质

MAX-HEAPIFY (A, i, root-index)
    l = left(i) + root-index
    r = right(i) + root-index
    heap-size = A.heap-size - root-index
    new-i = i + root-index

    if l - root-index < heap-size and A[l] > A[new-i]
        largest = l
    else
        largest = new-i

    if r - root-index < heap-size and A[r] > A[largest]
        largest = r

    if largest != new-i
        exchange A[new-i] with A[largest]
        MAX-HEAPIFY (A, largest, root-index)

对于一个以i为根节点,大小为n的子树,MAX-HEAPIFY的时间代价包括:
调整A[i]/A[left(i)]/A[right(i)]的关系的时间代价O(1)
在加上一棵以i的一个孩子节点为根节点的子树运行MAX-HEAPIFY的时间代价(假设递归调用会发生)
因为每个孩子子树的大小至多为2n/3(最坏情况发生在树的最底层恰好半满的时候)
T(n) <= T(2n/3) + O(1) ==> T(n) = O(lgn)
"
  (let ((heap-size (- (length arr) root-index))
    (l (+ (left i) root-index))
    (r (+ (right i) root-index))
    (new-i (+ i root-index))
    (largest (+ i root-index))
    (dummy nil))
    (if (and (< (- l root-index) heap-size) (> (aref arr l) (aref arr new-i)))
    (setf largest l))
    (if (and (< (- r root-index) heap-size) (> (aref arr r) (aref arr largest)))
    (setf largest r))
    (if (/= largest new-i)
    (progn
      (setf dummy (aref arr new-i))
      (setf (aref arr new-i) (aref arr largest))
      (setf (aref arr largest) dummy)
      (max-heapify arr largest root-index))
    arr)))

#+test
(let ((arr (vector 16 4 10 14 7 9 3 2 8 1)))
  ; arr result vector(16 14 10 8 7 9 3 2 4 1)
  (max-heapify arr 1))

#+test
(let ((arr (vector 16 4 10 14 7 9 3 2 8 1)))
  ; arr result vector(16 4 10 14 7 9 3 8 2 1)
  (max-heapify arr 0 7))

;;;; 6.3 建堆
(defun build-max-heap (arr root-index)
  ;i为相对数组里的下标,取值范围[0 ... ]
  (do ((i (- (length arr) 1) (1- i)))
      ((< i 0) arr)
    (max-heapify arr i root-index)))

#+test
(let ((arr (vector 4 1 3 2 16 9 10 14 8 7)))
  ; result: (vector 16 14 10 8 7 9 3 2 4 1)
  (build-max-heap arr))

#+test
(let ((arr (vector 4 1 3 2 16 9 10 14 8 7)))
  ; result: (vector 4 1 3 2 16 9 14 10 8 7)
  (build-max-heap arr 5))

; 每次调用 max-heapify的时间复杂度是lgn
; build-max-heap需要O(n)次这样的调用
; 因此总的时间复杂度就是O(nlgn)
; 这个上界虽然正确 但不是渐进紧确的
; 但是根据如下性质可以得到一个更紧确的界:
; 包含n个元素的堆的高度为floor(lgn),高度为h的堆最多包含celing(n/2^(h+1))个节点
; 最终可以推导出 build-max-heap为O(n)
; 目前不太清楚 build-max-heap => O(n) --!

;;;; 6.4 堆排序算法

(defun heap-sort (arr)
  "
build-max-heap初始化整体数组为最大堆
在相对数组里,首元素为最大,每次维护首元素下标后的元素的最大堆性质

HEAP-SORT(A)
    BUILD-MAX-HEAP(A, 0)
    for i = 0 until A.length
        MAX-HEAPIFY(A, 0, i)
"
  ;(do ((i 0 (1+ i)))
  ;    ((>= i (length arr)) arr)
  ;  (build-max-heap arr i)) ; 时间复杂度 O(n^2),弃用
  (build-max-heap arr 0) ;O(n) or O(nlgn),但最终对heap-sort的时间复杂度无影响
  (do ((i 1 (1+ i)))
      ((>= i (length arr)) arr)
    (max-heapify arr 0 i)) ;O(nlgn)
  )
    /*
     * 用数组存储二叉堆,用下标计算表示出对应的父结点,左孩子,右孩子
     */

    def parent(i: Int): Int = {
        i match {
            case i: Int if i <= 0 => 0
            case _ => math.floor((i - 1) / 2).toInt
        }
    }

    def left(i: Int): Int = (i * 2) + 1

    def right(i: Int): Int = (i + 1) * 2

    def exchange (arr: Array[Int], i: Int, j: Int) {
        val tmp = arr(i)
        arr(i) = arr(j)
        arr(j) = tmp
    }

    /**
      * 最大堆
      * @param arr      数组
      * @param i        相对下标,以根下标为准的新数组
      * @param root     根下标
      * @return
      */
    def `max-heapify` (arr: Array[Int], i: Int, root: Int = 0): Array[Int] = {
        val l = left(i) + root
        val r = right(i) + root
        val heapsize = arr.length - root
        val index = i + root

        var largest = index

        if (l - root < heapsize && arr(l) > arr(largest))
            largest = l

        if (r - root < heapsize && arr(r) > arr(largest))
            largest = r

        if (largest != index) {
            exchange(arr, index, largest)
            `max-heapify`(arr, largest, root)
        }
        arr
    }

    def `build-max-heap` (arr: Array[Int], root: Int = 0): Array[Int] = {
        for (i <- arr.length - 1 to 0 by -1) {
            `max-heapify`(arr, i)
        }
        arr
    }

    def `heap-sort` (arr: Array[Int]): Array[Int] = {
        `build-max-heap`(arr)
        for (i <- 0 to arr.length - 1) {
            `max-heapify`(arr, 0, i)
        }
        arr
    }

快速排序

;;;; 快速排序

; 快速排序的最坏时间复杂度为O(n^2),虽然最坏时间复杂度很差
; 但在实际排序应用中是最好的选择
; 它的期望复杂度是nlgn,而且也是原址排序


;;;; 7.1 快速排序描述

; 与归并排序一样,它采用了分治思想,下面是快速排序对数组A[l ... r]三步分治过程:

; 分解:数组A[l...r]被划分成为两个(可能为空)子数组A[l...m-1]和A[m+1...r]
; 使得A[l...m-1]中的任一元素都小于等于A[m],而A[m]也小于等于A[m+1...r]中的任一元素
; 其中,计算下标m也是划分的一部分

; 解决:通过递归调用快速排序,对子数组A[l...m-1]和A[m+1...r]进行排序

; 合并:因为子数组都是原址排序的,所以不需要合并操作:数组A[l...r]已经有序

(defun quick-sort (arr l r)
  "
QUICK-SORT(A, l, r)
    if l < r
        m = PARTITION(A, l, r)
        QUICK-SORT(A, l, m - 1)
        QUICK-SORT(A, m + 1, r)
"
  (let ((m nil))
    (if (< l r)
    (progn
      (setf m (partition arr l r))
      (quick-sort arr l (- m 1))
      (quick-sort arr (+ m 1) r)))
    arr))

(defun partition (arr l r)
  "
PARTITION(A, L, R)
    k = A[R]
    i = L
    for j = L to R - 1
        if A[j] <= x
            exchange A[i] with A[j]
            i = i + 1

    exchange A[i] with A[R]
    return i

大致思路就是这样,数组A[L...I...J...R]
区间[L...I]存放比key小或者等于的元素
区间[I...J]存放比key大的元素
区间[J...R]是未比较元素
当J接近到R时,由于最终有一步是i = i + 1
所以下标i此时的元素是大于key值的,交换他们之后
数组就变成这样的情况:
以下标i为界,前面部分都是比它小或者等的,后面部分都是大于它的
"
  (let ((k (aref arr r))
    (i l)
    (dummy nil))
    (do ((j l (1+ j)))
    ((>= j r))
      (if (< (aref arr j) k)
      (progn
        (setf dummy (aref arr i))
        (setf (aref arr i) (aref arr j))
        (setf (aref arr j) dummy)
        (setf i (+ i 1)))))
    (setf dummy (aref arr i))
    (setf (aref arr i) (aref arr r))
    (setf (aref arr r) dummy)
    (values i arr)))


;;;; 7.3 快速排序随机化版本

; 前面的快速排序中,输入数据的所有排列都是等概率的,但在实际工作中
; 这种情况不会总成立,所以在算法中引入随机化性,使得所有输入输入都能获得较好的期望性能
; 下面是随机版本的快速排序的伪代码实现:

; RANDOMIZED-PARTITION(A, p, r)
;     i = RANDOM(p, r)
;     exchange A[r] with A[i]
;     return PARTION(A, p, r)
; 
; RANDOMIZED-QUICKSORT(A, p, r)
;     if p < r
;         q = RANDOMIZED-PARTITION(A, p, r)
;         RANDOMIZED-QUICKSORT(A, p, q - 1)
;         RANDOMIZED-QUICKSORT(A, q + 1, r)
    def partition (arr: Array[Int], left: Int, right: Int): Int = {
        val k = arr(right)
        var i = left
        for (j <- left to right - 1) {
            if (arr(j) <= k) {
                /*exchange arr(i) and arr(j)*/
                val tmp = arr(i)
                arr(i) = arr(j)
                arr(j) = tmp

                i = i + 1
            }
        }
        arr(right) = arr(i)
        arr(i) = k
        i
    }

    def `quick-sort`(arr: Array[Int], left: Int, right: Int): Array[Int] = {
        if (left < right) {
            val mid = partition(arr, left, right)
            `quick-sort`(arr, left, mid - 1)
            `quick-sort`(arr, mid + 1, right)
        }
        arr
    }

计数排序

; 在排序的最终结果中,各元素之间的次序依赖于它们的比较关系,这样的排序算法称为`比较排序`
; 在之前笔记中涉及到的算法,包括`插入,归并,快速,堆`排序,都属于比较排序

; 计数排序运用的是运算而不是比较来确定排序顺序的

;;;; 8.2 计数排序

; 计数排序基本思想:对于每一个输入元素,确定小于x的元素的个数
; 利用这一信息,可以直接把元素放在数组对应的下标中

(defun counting-sort (arr)
  "
COUNTING-SORT(A, B, k)
    //数组B存放排序的输出
    let C[0...k] be a new array //提供临时存储空间
    for i = 0 until k
        C[i] = 0                //初始化C的元素

    for j = 0 until A.length
        C[A[j]] = C[A[j]] + 1   //C[i]代表A[n]元素个数(A[n] == i,i = 0, 1 ... k)

    for i = 0 until k
        C[i+1] = C[i+1] + C[i]  //对每一个i=0,1...k,统计多少输入元素是小于或等于i的

    for j = A.length - 1 downto 0
        B[C[A[j]] - 1] = A[j]
        C[A[j]] = C[A[j]] - 1   //把每个元素A[j]放到它在输出数值B中的正确位置
                                //如果所有元素互异,对于每一个A[j]值来说
                                //C[A[j]]就是A[j]在输出数值中的最终正确位置
                                //这是因为有C[A[j]]个元素小于或等于A[j]
                                //但有可能所有元素不是互异的
                                //所以将每一个值A[j]放入B中后,都要将C[A[j]]值减一
                                //这样,如果遇到下一个值等于A[j]的元素时
                                //可以直接放在输出数组B中A[j]的前一个位置

计数排序的一个重要性质是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的相对次序相同
代码实现上用到了比较性质,用于确定k值
而且由于编程语言的特性
加上对偏移的计算
实现上跟伪代码略微不同

显然,每重循环都是线性时间,最终时间复杂度表示为O(n)
但不是原址排序,空间换时间

如果计算出来的k值过大,也就是说存在过大的输入元素,就需要对空间和时间进行考虑了
"
  (labels ((maximum (arr max i)
         (if (< i 0)
         max
         (maximum arr
              (if (or (null max) (> (aref arr i) max))
                  (aref arr i)
                  max)
              (1- i))))
       (minimum (arr min i)
         (if (< i 0)
         min
         (minimum arr
              (if (or (null min) (< (aref arr i) min))
                  (aref arr i)
                  min)
              (1- i))))
       (fn-offset (num)
         (if (< num 0)
         (abs num)
         (- 0 num))))
    (let* ((len (length arr))
       (offset (fn-offset (minimum arr nil (1- len))))
       (k (+ 1 (+ (maximum arr nil (1- len)) offset)))
       (arr-tmp (make-array `(,k) :initial-element 0))
       (arr-out (make-array len)))
      (dotimes (j len)
    (setf (aref arr-tmp (+ offset (aref arr j))) (+ (aref arr-tmp (+ offset (aref arr j))) 1)))
      (dotimes (i (1- k))
    (setf (aref arr-tmp (+ i 1)) (+ (aref arr-tmp (+ i 1)) (aref arr-tmp i))))
      (do ((j (1- len) (- j 1)))
      ((< j 0) arr-out)
    (setf (aref arr-out (1- (aref arr-tmp (+ offset (aref arr j))))) (aref arr j))
    (setf (aref arr-tmp (+ offset (aref arr j))) (- (aref arr-tmp (+ offset (aref arr j))) 1))))))

or

    def `counting-sort` (a: Array[Int]): Array[Int] = {
        /*
         * 假设数据分布在[0 - 9]中,且数据个数不超过区间长度
         */
        val min = 0
        val max = 9

        val c = new Array[Int](max - min + 1)
        val b = new Array[Int](a.length)

        for (i <- 0 until c.length) c(i) = 0
        for (i <- 0 until a.length) c(a(i)) = c(a(i)) + 1
        for (i <- 1 until c.length) c(i) = c(i) + c(i - 1)
        for (i <- a.length - 1 to 0 by -1) {
            b(c(a(i)) - 1) = a(i)
            c(a(i)) = c(a(i)) - 1
        }
        b
    }

上一篇下一篇

猜你喜欢

热点阅读