《算法导论》一,二部分算法浅析及其JavaScript实现

623 阅读11分钟

本文不讨论算法复杂度及其正确性证明过程,只讨论算法思想及其实现

目录概览

插入排序

什么是插入排序

算法导论中举了一个特别棒的例子

算法过程

下图记录了一个排序的过程
a)首先2和5比较 2比5小2和5交换位置
b)4和5比较 5比4大  然后和2比较 然后插入当前位置 5往后顺延
......
一直往前比较 直到比较的数字比这个数字小

算法实现(javascript版本)

function InsertionSort(arr) {
  for (let j = 1; j < arr.length; j++) {
    let key = arr[j]; // 用来作为一个暂存变量
    let i = j - 1; 
    //下面这一段的代码 是找到其正确的位置插入进去
    while (i >= 0 && arr[i] > key) {
      arr[i + 1] = arr[i] //顺位移动
      i = i - 1
    }
    arr[i + 1] = key
  }
  return arr
}

归并排序(MergeSort)

什么是归并排序


先理解一个概念,算法中有一种设计思想叫分治法

在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

算法思路


归并排序就是

  • 分解待排序的n各元素序列成各具n/2个元素的两个子序列
  • 递归两个子序列做相同的操作
  • 合并已经排序的两个子序列


第三步 可以单独拆成一个问题看 有一个leetcode的题目 合并两个有序数组

算法过程


算法导论中的归并排序过程如下图所示


归并的过程 = 合并两个有序数组(算法导论中思路非最优解,所以下面的过程是有优化空间的)
如下图所示例子

具体实现


javascript版本


//分解 递归的过程 对应图1
function mergeSort(arr) {
  let len = arr.length
  if (len < 2) return arr
  let middle = Math.floor(len / 2)
  let left = arr.slice(0, middle)
  let right = arr.slice(middle)
  return merge(mergeSort(left), mergeSort(right))
}

//合并有序数组的过程 对应图2
function merge(left, right) {
  let result = []
  let i = 0
  let j = 0
  while ((i <= left.length - 1) && (j <= right.length - 1)) {
    if (left[i] <= right[j]) {
      result.push(left[i])
      i = i + 1
    } else {
      result.push(right[j])
      j = j + 1
    }
  }
  while (i <= left.length - 1) {
    result.push(left[i])
    i = i + 1
  }
  while (j <= right.length - 1) {
    result.push(right[j])
    j = j + 1
  }
  return result
}

最大子数组


#### 最大子数组问题介绍


算法导论上举了一个例子



实际上问题就是 寻找数组A的最大非空连续子数组

算法思路


分治法
假设将原数组一分为2的话,我们要寻找的最大子数组他所处的位置只有三种情况
1.最大子数组完全在左边


2.最大子数组完全在右边


3.最大子数组穿越左边和右边


然后我们只要比较这三个和的大小就可以知道最大的是哪一组了

算法过程


假定一个求解方法findMaxSubarray,
求解左边最大子数组findMaxSubarray(left)
求解右边最大子数组findMaxSubarray(right)
我们还需要一个求得最大交叉数组的方法findMaxCrossingSubarray

过程如下图所示 从中间分别往两边遍历累加,分别记住两边和最大的位置然后在合并得到的就是交叉最大子数组

算法实现

javascript版本

function findMaxCrossingSubarray(left, right) {
    const L = left.length
    const R = right.length
    let sum = left[L - 1]
    let leftSum = left[L - 1], rightSum = right[0]
    let maxLeft = L-1
    let maxRight = 0
    for (let i = L - 2; i >= 0; i--) {
        sum = left[i] + sum
        if (leftSum < sum) {
            leftSum = sum
            maxLeft = i
        }
    }
    sum = right[0]
    for (let j = 1; j < R; j++) {
        sum = right[j] + sum
        if (rightSum < sum) {
            rightSum = sum
            maxRight = j
        }
    }
    return [left.slice(maxLeft, L).concat(right.slice(0, maxRight + 1)), leftSum + rightSum]
}
function findMaxSubarray(arr) {
    let newArr = [...arr]
    let len = newArr.length
    if (len < 2) return [newArr, newArr[0]]
    let middle = Math.floor(len / 2)
    let left = newArr.slice(0, middle)
    let right = newArr.slice(middle, len)
    let [leftArr, leftSum] = findMaxSubarray(left)
    let [rightArr, rightSum] = findMaxSubarray(right)
    let [crossArr, crossSum] = findMaxCrossingSubarray(left, right)
    if (leftSum >= rightSum && leftSum >= crossSum) {
        return [leftArr, leftSum]
    } else if (rightSum >= leftSum && rightSum >= crossSum) {
        return [rightArr, rightSum]
    } else {
        return [crossArr, crossSum]
    }
}
module.exports = findMaxSubarray

Strassen算法

Strassen算法是干啥的


矩阵乘法正常来说需要n^3的时间复杂度
先看一个矩阵乘法的公式

因此,我们可以联想到 两个矩阵的相乘可以拆解为四个(四块颜色)小矩阵的互相加乘

所以可以得到这样的递归式(伪代码)

但是时间复杂度任然为n^3,而利用某个规则可以修改这个递归式 让他少发生矩阵的相乘运算
最终可以使时间复杂度降低至n^lg7 ~=2.8

算法思路


这里直接给出公式 证明过程。。。。你懂的







算法过程

  1. 需要实现一个初始化矩阵的方法
  2. 需要实现一个分区矩阵的方法(注意:这里其实是会产生问题的,算法导论中提及需要通过矩阵下标去运算而不是复制,本人这里只实现了复制的)
  3. 需要实现一个矩阵的加减运算
  4. 需要实现一个将四个分区合并的方法
  5. 需要实现一个递归方法

算法实现

//  大前提 假定nxn的矩阵 n是2的幂

/**
 * 初始化矩阵
 * @param {*} l - n阶矩阵
 */
function initMatrix(l) {
    let r = [];
    for (let i = 0; i < l; i++) {
        r.push([])
    }
    return r
}

/**
 * 根据区域块截取(分成4部分)
 * @param {*} A - 原矩阵
 * @param {*} a - 1或者2
 * @param {*} b - 1或者2
 */
function sliceMatrix(A, a, b) {
    let n = A.length / 2
    let matrix = initMatrix(n)
    let x = 0, y = 0;
    for (let j = (a - 1) * n; j < a * n; j++) {
        for (let i = (b - 1) * n; i < b * n; i++) {
            matrix[x][y] = A[j][i]
            ++y
        }
        ++x
    }
    return matrix
}
// 加减运算
function MatrixPM(A, B, type) {
    //代码 省略
    let n = A.length
    let rt = initMatrix(n)
    for (let j = 0; j < n; j++) {
        for (let i = 0; i < n; i++) {
            if (type == '-') {
                rt[j][i] = A[j][i] - B[j][i]
            } else {
                rt[j][i] = A[j][i] + B[j][i]
            }
        }
    }
    return rt
}
// 合并矩阵
function MergeMatrix(A, B, C, D) {
    let n = A.length;
    let matrix = initMatrix(2 * n)
    for (let j = 0; j < n; j++) {
        for (let i = 0; i < n; i++) {
            matrix[j][i] = A[j][i]
            matrix[j][i + n] = B[j][i]
            matrix[j + n][i] = C[j][i]
            matrix[j + n][i + n] = D[j][i]
        }
    }
    return matrix
}
function Strassen(A, B) {
    if (A.length == 1) {
        return [[A[0] * B[0]]]
    }
    let n = A.length;
    let s1 = MatrixPM(sliceMatrix(B, 1, 2), sliceMatrix(B, 2, 2), '-');
    let s2 = MatrixPM(sliceMatrix(A, 1, 1), sliceMatrix(A, 1, 2), '+');
    let s3 = MatrixPM(sliceMatrix(A, 2, 1), sliceMatrix(A, 2, 2), '+');
    let s4 = MatrixPM(sliceMatrix(B, 2, 1), sliceMatrix(B, 1, 1), '-');
    let s5 = MatrixPM(sliceMatrix(A, 1, 1), sliceMatrix(A, 2, 2), '+');
    let s6 = MatrixPM(sliceMatrix(B, 1, 1), sliceMatrix(B, 2, 2), '+');
    let s7 = MatrixPM(sliceMatrix(A, 1, 2), sliceMatrix(A, 2, 2), '-');
    let s8 = MatrixPM(sliceMatrix(B, 2, 1), sliceMatrix(B, 2, 2), '+');
    let s9 = MatrixPM(sliceMatrix(A, 1, 1), sliceMatrix(A, 2, 1), '-');
    let s10 = MatrixPM(sliceMatrix(B, 1, 1), sliceMatrix(B, 1, 2), '+');
    let p1 = Strassen(sliceMatrix(A, 1, 1), s1)
    let p2 = Strassen(s2, sliceMatrix(B, 2, 2))
    let p3 = Strassen(s3, sliceMatrix(B, 1, 1))
    let p4 = Strassen(sliceMatrix(A, 2, 2), s4)
    let p5 = Strassen(s5, s6)
    let p6 = Strassen(s7, s8)
    let p7 = Strassen(s9, s10)
    let c11 = MatrixPM(MatrixPM(MatrixPM(p5, p4, '+'), p2, '-'), p6, '+')
    let c12 = MatrixPM(p1, p2, '+')
    let c21 = MatrixPM(p3, p4, '+')
    let c22 = MatrixPM(MatrixPM(MatrixPM(p1, p5, '+'), p3, '-'), p7, '-')
    return MergeMatrix(c11, c12, c21, c22)
}
module.exports = Strassen

堆排序

什么是堆排序

堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。



在javascript中我们用数组来实现这一种数据结构
堆排序就是利用这种数据结构进行排序

算法思路


就是建立一个最大堆,然后利用堆顶永远是最大数值来排序
如下图所示,每次都将黄色区域的值(堆顶)与粉红区域的值(末位)进行交换

然后排除末位  在剩下的数组中进行最大堆重塑
如此往复,到这个堆只剩下一个元素 就完成了整个排序过程

算法过程

  1. 首先定义一个排序方法 HeapSort
  2. 然后我们需要建立一个最大堆BuildMaxHeap
  3. 然后我们需要一个维护堆的性质的方法,也就是重塑堆得方法MaxHeap
  4. 往复的从堆顶与数组末位互换位置 然后重塑


BuildMaxHeap的前提是我们要先把数组看成一个堆,看成堆了之后我们要知道数组中的位置和堆的位置的对照关系,如下图所示

我们以堆底开始利用MaxHeap构建最大堆
MaxHeap是假设存在一个堆,它的堆顶的左节点和右节点都是符合最大堆的性质的

算法实现

/**
 * 时间复杂度 平均:O(nlog2n)。
 * 空间复杂度:O(1)。
 * 稳定性:不稳定
 */

function HeapSort(arr) {
	var len = arr.length;
	BuildMaxHeap(arr,len);
	for (var i = len-1 ; i > 0; i--) {
		let box = arr[0];
		arr[0] = arr[i];
		arr[i] = box;
		MaxHeap(arr,0,i);
	}
	return arr
}

function MaxHeap(arr,i,length) {
	var largest = null;
	var node = arr[i]; //保存当前节点
	var left = i * 2 + 1 ; //定位节点左
	var right = i * 2 + 2; //定位节点右	
	//判断当前有这个节点 (这里会存在当前这个的子节点不存在的情况)处理一下边界情况
	if (left < length && node < arr[left]) {
		largest = left
	}else{
		largest = i;
	}
	if (right < length && arr[largest] < arr[right]) {
		largest = right
	}
	//如果不是i是最大节点 以node作为辅助节点 交换位置
	if (largest != i) {
		arr[i] = arr[largest];
		arr[largest] = node;
		MaxHeap(arr,largest,length);
	}
}
//建立一个最大堆
function BuildMaxHeap(arr,len){
	if(len%2!=0){
		len = len +1 ;
	}
	for(let i = len/2;i>=0;i--){
		MaxHeap(arr,i,len)
	}
}

优先队列

什么是优先队列


算法导论中的定义,如下图所示

算法过程


优先队列是用堆来实现的,刚好我们上面的堆排序只要稍加改造就可以实现一个最大堆从而实现一个优先队列:)有了之前堆排序的基础 这个的实现就很容易理解了
MAXIMIUM:只要取堆顶的元素就可以了
extractMax:只要取堆顶元素然后把该元素踢出 然后进行一次堆得维护就好了
increase:  只要不停的与父节点进行比较,若比父节点大则互换位置。若比父节点小则位置恰好,结束循环。
insert:有了increase后只要在堆尾新增一个元素,将其优先级设为负数(表示最小优先级),然后使用increase方法提升优先级就可以了

算法实现

class PriorityQueue {

    constructor(arr) {
        this.queue = []
        if (arr && arr instanceof Array && arr.length >= 1) {
            this.create(arr)
        } else {
            throw new Error('非法传入值')
        }
    }
    // 实现一个简单的拷贝
    copy(input) {
        return JSON.parse(JSON.stringify(input))
    }
    //获取最高优先级的内容
    MAXIMIUM() {
        return this.copy(this.queue[0])
    }
    //插入一个新的元素
    insert(target) {
        if (!target || !target.priority) return false
        let temp = target.priority
        target.priority = -1
        this.queue.push(target)
        this.increase(this.queue.length-1,temp)
    }
    //提升一个元素的优先级
    increase(index, newPriority) {
        if (newPriority <= this.queue[index].priority) return
        this.queue[index].priority = newPriority
        const findParent = (index) => {
            if (index == 0) {
                return null
            }
            return index % 2 == 0 ? (index - 2) / 2 : (index - 1) / 2
        }
        let parent = findParent(index)
        while (parent != null && this.queue[parent].priority < this.queue[index].priority) {
            let box = this.queue[index]
            this.queue[index] = this.queue[parent]
            this.queue[parent] = box
            index = parent
            parent = findParent(parent)
        }
    }
    //获取最高优先级的内容 然后踢出队列
    extractMax() {
        let max = this.copy(this.queue[0])
        this.queue.shift()
        if (this.queue.length != 1) {
            this.MaxHeap(this.queue, 0, this.queue.length)
        }
        return max
    }

    //提供一个初始化方法 建堆
    create(arr) {
        this.queue = this.copy(arr)
        this.BuildMaxHeap(this.queue, this.queue.length)
    }

    MaxHeap(arr, i, length) {
        var largest = null;
        var node = arr[i]; //保存当前节点
        var left = i * 2 + 1; //定位节点左
        var right = i * 2 + 2; //定位节点右	
        //判断当前有这个节点 (这里会存在当前这个的子节点不存在的情况)处理一下边界情况
        if (left < length) {
            arr[left] && node.priority < arr[left].priority ? largest = left : largest = i
        } else {
            largest = i;
        }
        if (right < length) {
            arr[right] && arr[largest].priority < arr[right].priority ? largest = right : null
        }
        //如果不是i是最大节点 以node作为辅助节点 交换位置
        if (largest != i) {
            arr[i] = arr[largest];
            arr[largest] = node;
            this.MaxHeap(arr, largest, length);
        }
    }

    //建立最大堆
    BuildMaxHeap(arr, len) {
        if (len % 2 != 0) {
            len = len + 1;
        }
        for (let i = len / 2; i >= 0; i--) {
            this.MaxHeap(arr, i, len)
        }
    }

}
let pq = new PriorityQueue([
    { priority: 1, todo: '吃饭' },
    { priority: 2, todo: '洗澡' },
    { priority: 3, todo: '睡觉' },
    { priority: 4, todo: '玩手机' },
    { priority: 5, todo: '打篮球' },
    { priority: 6, todo: '打乒乓' },
    { priority: 7, todo: '喝水' }
])
console.log(pq.queue)
pq.increase(6, 8)
console.log(pq.queue)
pq.insert({priority: 9, todo: '我是新加的'})
console.log(pq.queue)

快速排序

快速排序简介


快速排序是一种最坏情况时间复杂度位n^2的排序方法。虽然最坏情况下的复杂度很差,但是快速排序通常是实际应用排序中比较好的选择。因为它的平均性能是非常好的nLgn。

算法思路

  1. 选出某一个数组中的数值作为标准。
  2. 把小于标准值的放到数组左边,大于标准值得放到数组右边。
  3. 对上述过程进行递归直到不能分解为止

算法过程

  1. 我们定义一个快速排序的方法 QuickSort,我们还需要传入排序的起点和终点以便每一次的递归
  2. 我们需要一个方法去执行上述的思路中的1,2步骤 最后返回一个中间值得坐标 以便把数组 区分左右所在位置
  • 定义这个方法为 partition ,它的实现过程是这样的
  • 取最后一位作为它的基准值 也就是图中的r
  • 定义两个指针ij
  • j指针依次遍历,将当前指针的值与标准值r相比较
  • 若j指针的值大于r则继续遍历,若j指针的值小于等于r 那么i指针向前一步 然后i,j指针所在位置的值互换位置
  • 从上述的过程我们可以理解到i指针就是两个数组的边界
  • 当j指针遍历完之后,将r插入到i后面即完成了整个partition过程


image.png

算法实现


这里快速排序是实现的原址排序

function exchange(arr, a, b) {
    let tem = arr[a]
    arr[a] = arr[b]
    arr[b] = tem
}
function partition(input, l, r) {
    let s = input[r]
    let i = l - 1
    for (let j = l; j < r; j++) {
        if (input[j] <= s) {
            ++i
            exchange(input, i, j)
        }
    }
    exchange(input, i + 1, r)
    return i + 1
}
function QuickSort(arr, l, r) {
    if (l < r) {
        let mid = partition(arr, l, r)
        QuickSort(arr, l, mid - 1)
        QuickSort(arr, mid + 1, r)
    }
}
module.exports = QuickSort

计数排序

啥是计数排序


计数排序有点类似于穷举的味道,我们把数字中每个数字出现的次数都记录下来最后只要依次在组合起来。比如下图 9 出现了两次 就在数组中放入两个9

image.png

算法过程

  1. 获取数组中的最大值
  2. 把他们分类整理并记录每个数字出现的次数
  3. 重新整理输出数组

算法实现

function CountingSort(arr) {
    let len = arr.length;
    let max = Math.max.apply(null, arr);
    let temp = new Array(max + 1).fill(null);
    let result = [];
    for (let i = 0; i < len; i++) {
        temp[arr[i]] = temp[arr[i]] ? temp[arr[i]] + 1 : 1
    }
    temp.forEach((e, index) => {
        if (e != null) {
            for (let i = 0; i < e; i++) {
                result.push(index)
            }
        }
    })
    return result
}
module.exports = CountingSort

基数排序

什么是基数排序


如下图所示,我们对个位,十位,百位依次进行排列,然后就可以得到一个完整的排序过程

算法过程

  1. 因为每一个基数只可能是0-9所以可以对每一列进行计数排序
  2. 循环所有的列

算法实现

function createZero(num, targetLength) {
    let t = targetLength - num.toString().length
    let str = ''
    while (t > 0) {
        t--;
        str += '0'
    }
    return str + num.toString()
}

function RadixSort(arr) {
    let len = arr.length
    let max = Math.max.apply(null, arr)
    const targetLength = max.toString().length
    let strArr = arr.map(t => createZero(t,targetLength))
    let map = {}
    for (let j = targetLength - 1; j >= 0; j--) {
        for (let i = 0; i < len; i++) {
            map[Number(strArr[i][j])] ? map[Number(strArr[i][j])].push(strArr[i]) : map[Number(strArr[i][j])] = [strArr[i]]
        }
        let temp = [];
        for (let q = 0; q < 10; q++) {
            temp = map[q] ? temp.concat(map[q]) : temp
        }
        map = []
        strArr = temp
    }
    let result = strArr.map(s => Number(s))
    return result
}

module.exports = RadixSort

桶排序

什么是桶排序


桶排序可以看做是计数排序的一种升级方式
桶排序是指将一组数据按照一定的区间进行分类,可以简单理解就是看第一位数子 把他们分类。

然后在每个桶内进行排序 此时你可以使用任意的排序方法
桶排序对数据有限制 如果不是同等位数的 数字的话 桶排序就没有太大的意义了

算法过程

  1. 将数据分类
  2. 组内排序
  3. 组合

算法实现

function BucketSort(arr) {
    let l = arr.length
    if (l <= 1) return arr
    let max = Math.max.apply(null, arr)
    let result = []
    let buckets = new Array(l).fill([])
    for (let i = 0; i < l; i++) {
        let BucketIndex = parseInt(arr[i].toString()[0])
        buckets[BucketIndex].length == 0 ? buckets[BucketIndex] = [arr[i]] : buckets[BucketIndex].push(arr[i])
    }
    buckets.forEach(e => {
        QuickSort(e, 0, e.length - 1)
        result = result.concat(e)
    })
    return result
}

module.exports = BucketSort