排序算法
利用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
}
冒泡排序算法的可优化点:
-
如果某一轮排序,没有发生交换的过程。说明元素已经有序,直接退出循环结束排序。
-
在比较交换的过程中,若后面的元素有序。可记录上一次交换的位置,作为已排序元素的边界。下一轮冒泡的过程将截止到上一次交换的位置,减少已排序元素的比较次数。
-
采用双向冒泡排序,即在一次排序过程中来回进行两次冒泡比较。这个排序称为鸡尾酒排序。
三次优化后冒泡排序如下:
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
}
插入排序
实现步骤如下:
- 记录i,j,待排队元素(第二位开始)。
- 待排队元素不断与前面元素对比换位。
- 当j=0终止对比,换下一个待排队元素。
- 重复上面的步骤,直至最后一个元素。
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
}
快速排序(分治思想)
实现步骤如下:
- 在数组中选定一个基准值,然后开始遍历数组(已选用
array[0],因此从1开始遍历) - 若当前值
array[i]小于基准值array[0],将其推入左边的数组。 - 若当前值
array[i]大于或等于基准值array[0],将其推入右边的数组。 - 分别对左边和右边的数组同样递归进行上述步骤,且每一轮递归都要合并(concat)左右数组和基准值。
- 当数组长度小于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))
}
归并排序(分治思想)
实现步骤如下:
- 将待排序数组中间开始分割成左右数组两个数组,然后进行合并(merge)。
- 合并(merge)的过程中使用
i和j作为指针分别标识左右数组第一个元素,遍历两个数组中的元素开始比较。 - 两个数组中最小的元素先放入结果,对应的指针指向下一位继续比较(i++/j++)。否则,指针保持不变
- 若比较过程中两个指针的其中之一等于数组长度,说明该指针对应的数组已经没有元素可以用于比较。此时必然存在另外一个指针不等于数组长度,而该指针对应数组的剩余元素需要与结果合并(concat)。(注意:每次合并都是有序的)
- 分别对左边和右边的数组同样递归分割进行上述步骤,且每一轮递归都要合并(merge)左右的数组。
- 当数组长度小于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
}
查找算法
二分查找
实现步骤如下:
- 通过数组初始序号和结尾序号获取中间序号。
mid = Math.floor((start + end) / 2) - 若待查找数值比当前中间序号的数组值大。那么往右边查找,
start = mid + 1 - 若待查找数值比当前中间序号的数组值大。那么往左边查找,
end = mid - 1 - 重复以上的步骤,直到找到值(出现
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仓库 👇