算法TS之排序和查找实现

427 阅读2分钟

排序算法

利用TypeScript实现排序算法前先设定可排序的元素类型和一个默认比较函数。

// 定义回调函数
type sortedCallbackFuction<T> = (a: T, b: T) => number
// 默认按字符串编码排序
const defaultCompare: sortedCallbackFuction<any> = (a, b): number => {
    return a.toString().charCodeAt(0) - b.toString().charCodeAt(0)
}

冒泡排序

冒泡排序顾名思义,元素在排序过程中会像气泡一样不断上升。

第一个for循环:每一轮都进行排序,n个元素需要n-1次排序。排序次数array.length - 1

第二个for循环:每一轮都将排序好一个元素,剩余元素之间的比较次数array.length - i - 1

function bubbleSort<T>(
    array: T[],
    compare: sortedCallbackFuction<T> = defaultCompare): T[] {
    for (let i = 0; i < array.length - 1; i++) {
        for (let j = 0; j < array.length - i - 1; j++) {
            if (compare(array[j + 1], array[j]) < 0) {
                ([array[j], array[j + 1]] = [array[j + 1], array[j]])
            }
        }
    }
    return array
}

冒泡排序算法的可优化点:

  1. 如果某一轮排序,没有发生交换的过程。说明元素已经有序,直接退出循环结束排序。

  2. 在比较交换的过程中,若后面的元素有序。可记录上一次交换的位置,作为已排序元素的边界。下一轮冒泡的过程将截止到上一次交换的位置,减少已排序元素的比较次数。

  3. 采用双向冒泡排序,即在一次排序过程中来回进行两次冒泡比较。这个排序称为鸡尾酒排序。

三次优化后冒泡排序如下:

function bubbleSort<T>(array: T[], compare: sortedCallbackFuction<T> = defaultCompare): T[] {
    let lastSwapLeftIndex = 0
    let lastSwapRightIndex: number = array.length - 1
    let leftBorder: number = lastSwapLeftIndex
    let rightBorder: number = lastSwapRightIndex
    for (let i = 0; i < array.length / 2; i++) {
        let isSorted = true
        for (let j = leftBorder; j < rightBorder; j++) {
            if (compare(array[j + 1], array[j]) < 0) {
                ;[array[j], array[j + 1]] = [array[j + 1], array[j]]
                isSorted = false
                lastSwapRightIndex = j
            }
        }
        if (isSorted) {
            break
        }
        rightBorder = lastSwapRightIndex
        for (let j = rightBorder; j > leftBorder; j--) {
            if (compare(array[j - 1], array[j]) > 0) {
                ;[array[j], array[j - 1]] = [array[j - 1], array[j]]
                isSorted = false
                lastSwapLeftIndex = j
            }
        }
        if (isSorted) {
            break
        }
        leftBorder = lastSwapLeftIndex
    }
    return array
}

插入排序

实现步骤如下:

  1. 记录i,j,待排队元素(第二位开始)。
  2. 待排队元素不断与前面元素对比换位。
  3. 当j=0终止对比,换下一个待排队元素。
  4. 重复上面的步骤,直至最后一个元素。
function insertSort<T>(array: T[], compare: sortedCallbackFuction<T> = defaultCompare): T[] {
    for (let i = 1; i < array.length; i++) {
        for (let j = i; j > 0; j--) {
            if (compare(array[j], array[j - 1]) < 0) {
                ;[array[j - 1], array[j]] = [array[j], array[j - 1]]
            } else {
                break
            }
        }
    }
    return array
}

快速排序(分治思想)

实现步骤如下:

  1. 在数组中选定一个基准值,然后开始遍历数组(已选用array[0],因此从1开始遍历)
  2. 若当前值array[i]小于基准值array[0],将其推入左边的数组。
  3. 若当前值array[i]大于或等于基准值array[0],将其推入右边的数组。
  4. 分别对左边和右边的数组同样递归进行上述步骤,且每一轮递归都要合并(concat)左右数组和基准值。
  5. 当数组长度小于2时,停止递归并返回当前数组。函数调用栈出栈,不断合并(concat)之前分割的数组
function quickSort<T>(array: T[], compare: sortedCallbackFuction<T> = defaultCompare): T[] {
    if (array.length < 2) return array
    const left: T[] = [], right: T[] = []
    for (let i = 1; i < array.length; i++) {
        if (compare(array[0], array[i]) <= 0) {
            right.push(array[i])
        } else {
            left.push(array[i])
        }
    }
    return quickSort(left, compare).concat(array[0], quickSort(right, compare))
}

归并排序(分治思想)

实现步骤如下:

  1. 将待排序数组中间开始分割成左右数组两个数组,然后进行合并(merge)。
  2. 合并(merge)的过程中使用ij作为指针分别标识左右数组第一个元素,遍历两个数组中的元素开始比较。
  3. 两个数组中最小的元素先放入结果,对应的指针指向下一位继续比较(i++/j++)。否则,指针保持不变
  4. 若比较过程中两个指针的其中之一等于数组长度,说明该指针对应的数组已经没有元素可以用于比较。此时必然存在另外一个指针不等于数组长度,而该指针对应数组的剩余元素需要与结果合并(concat)。(注意:每次合并都是有序的)
  5. 分别对左边和右边的数组同样递归分割进行上述步骤,且每一轮递归都要合并(merge)左右的数组。
  6. 当数组长度小于1时,停止递归分割数组。函数调用栈出栈,之前分割的数组开始合并(merge)操作。

问题:如何理解归并排序中的递归过程?

最重要的是理解函数调用栈的进栈出栈操作。函数递归终止时会将结果按出栈的顺序一层层往前传递。

function merge<T>(left: T[], right: T[], compare: sortedCallbackFuction<T> = defaultCompare): T[] {
    let i = 0, j = 0, end: T[] = []
    const result: T[] = []
    while (i < left.length && j < right.length) {
        if (compare(left[i], right[j]) <= 0) {
            result.push(left[i++])
        } else {
            result.push(right[j++])
        }
    }
    if (i < left.length && j === right.length) {
        end = left.slice(i)
    } else if (j < right.length && i === left.length) {
        end = right.slice(j)
    }
    return result.concat(end)
}
function mergeSort<T>(array: T[], compare: sortedCallbackFuction<T> = defaultCompare): T[] {
    if (array.length > 1) {
        const { length } = array
        const middle = Math.floor(length / 2)
        const left: T[] = mergeSort(array.slice(0, middle), compare)
        const right: T[] = mergeSort(array.slice(middle, length), compare)
        array = merge(left, right, compare)
    }
    return array
}

查找算法

二分查找

实现步骤如下:

  1. 通过数组初始序号结尾序号获取中间序号mid = Math.floor((start + end) / 2)
  2. 待查找数值当前中间序号的数组值大。那么往右边查找,start = mid + 1
  3. 待查找数值当前中间序号的数组值大。那么往左边查找,end = mid - 1
  4. 重复以上的步骤,直到找到值(出现target === index的情况)或找不到值(出现start > end的情况)
function isObject(obj: number | object, key: string): boolean {
    return (
        obj !== null && typeof obj === 'object' && typeof obj[key] === 'number'
    )
}
function binarySearch<T>(
    array: T[],
    target: number | { [key: string]: number }
): number | boolean {
    const key = Object.keys(target)[0]
    const bool = isObject(target, key)
    array = quickSort<any>(array, (a, b) => {
        return bool ? a[key] - b[key] : a - b
    })
    let start = 0, end: number = array.length - 1
    target = bool ? target[key] : target
    while (start <= end) {
        const mid = Math.floor((start + end) / 2)
        const index = bool ? array[mid][key] : array[mid]
        if (target > index) {
            start = mid + 1
        } else if (target < index) {
            end = mid - 1
        } else if (target === index) {
            return mid
        }
    }
    return false
}

这里推荐两个网站,第一个网站可以动态演示排序算法,第二个网站可以查看常用数据结构和算法的时间复杂度。

排序算法动画网站:visualgo.net/en/sorting

Big-O备忘录网站:www.bigocheatsheet.com/

本文相关代码已放置我的Github仓库 👇


项目地址:Algorithmlib|Sort Algorithmlib|Search