前端基础算法

排序

公共方法

交换arr里面i1索引与i2索引的数据的位置

function exchange(arr, i1, i2) {
	let tmp = arr[i1];
	arr[i1] = arr[i2];
	arr[i2] = tmp;
}

选择排序

选择排序就是通过不断遍历数组未排序部分找到最小值,将其放在未排序部分的最前面

function selectSort(arr) {
	let len = arr.length;
	// 当前边的都排好以后,最后一位一定是最大值,所以i < len - 1即可
	for (let i = 0; i < len - 1; i++) {
		let min = arr[i];
		let minIndex = i;
		// 找的是还没有排序的数据的最小值,所以j从i + 1开始
		for (let j = i + 1; j < len; j++) {
			if (arr[j] < min) {
				min = arr[j];
				minIndex = j;
			}
		}
		exchange(arr, i, minIndex);
	}
}

冒泡排序

冒泡排序就是从数组前两位开始,将大的那个放在后面,然后比较二三位,直到将最大的放在末尾

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

插入排序

插入排序类似斗地主摸牌的过程,默认第一位是已经拍好顺序的,将后面的每一位依次插入到已排序部分

function insertionSort(arr) {
	let len = arr.length;
	for (let i = 1; i < len; i++) {
	    // 如果插入项本就大于最后一项,那么不管,直接放在最后
		if (arr[i] < arr[i - 1]) {
			let tmp = arr[i];
			// 寻找插入位置
			for (let j = i; j >= 0; j--) {
				if (j > 0 && arr[j - 1] > tmp) {
					arr[j] = arr[j - 1];
				} else {
					arr[j] = tmp;
					break;
				}
			}
		}
	}
}

快速排序

将数组第一个或最后一个定位基准值,通过与基准值比较,将比基准值小的放在左侧,比基准值大的放在右侧,然后继续递归

function quickSort(arr) {
	function _quickSort(arr, low, high) {
		let left = low;
		let right = high;
		// 如果只有一项,直接返回
		if (low >= high) return;
		// 如果记录high为基准值,那么先移动low指针
		// 因为先移动high指针的话low位置的值将被覆盖,但是high位置的值已经被存下,被覆盖也没关系
		let tmp = arr[high];
		while (low < high) {
			while (low < high && arr[low] < tmp) low++;
			arr[high] = arr[low];
			while (low < high && arr[high] >= tmp) high--;
			arr[low] = arr[high];
		}
		arr[low] = tmp;
		_quickSort(arr, left, low - 1);
		_quickSort(arr, low + 1, right);
	}
	_quickSort(arr, 0, arr.length - 1);
}

查找

遍历查找

遍历查找是最简单直接的查找方式,适用于任何数组,但是查找次数较大

function search(arr, target) {
	for (let i = 0; i < arr.length; i++) {
		searchCount++;
		if (arr[i] == target) return i;
	}
	return false;
}

二分查找

二分查找是速度较快的查找方式,但是前提是数组是排列好的

// 二分查找 循环
function binarySearch(arr, target) {
	if (arr.length == 0 || target < arr[0] || target > arr[arr.length - 1])
		return false;
	function _binarySearch(arr, target, start, end) {
		while (start < end) {
			searchCount++;
			let mid = parseInt((start + end) / 2);
			if (arr[mid] == target) {
				return mid;
			} else if (arr[mid] < target) {
				start = mid + 1;
			} else if (arr[mid] > target) {
				end = mid - 1;
			}
		}
		return false;
	}
	return _binarySearch(arr, target, 0, arr.length - 1);
}
function binarySearch2(arr, target) {
	if (arr.length == 0 || target < arr[0] || target > arr[arr.length - 1])
		return false;
	function _binarySearch(arr, target, start, end) {
		while (start < end) {
			searchCount++;
			let mid = parseInt((start + end) / 2);
			if (arr[mid] == target) {
				return mid;
			} else if (arr[mid] < target) {
				return _binarySearch(arr, target, mid + 1, end);
			} else if (arr[mid] > target) {
				// end = mid - 1;
				return _binarySearch(arr, target, start, mid - 1);
			}
		}
		return false;
	}
	return _binarySearch(arr, target, 0, arr.length - 1);
}

插值查找

插值查找对排列好,并且相邻的值的大小差距差不多的数组有很快的查找速度,相比二分查找,优化了mid的取值

function interpolationSearch(arr, target) {
	if (arr.length == 0 || target < arr[0] || target > arr[arr.length - 1])
		return false;
	function _binarySearch(arr, target, start, end) {
		while (start < end) {
			searchCount++;
			let mid = parseInt(
				((target - arr[start]) * (end - start)) /
					(arr[end] - arr[start]) +
					start
			);
			if (arr[mid] == target) {
				return mid;
			} else if (arr[mid] < target) {
				return _binarySearch(arr, target, mid + 1, end);
			} else if (arr[mid] > target) {
				// end = mid - 1;
				return _binarySearch(arr, target, start, mid - 1);
			}
		}
		return false;
	}
	return _binarySearch(arr, target, 0, arr.length - 1);
}

树的特点

  1. 单根:如果一个节点A指向节点B,那么仅可通过A找到B,不可能通过其他节点找到B
  2. 无环:节点的指向不能形成环

树的术语

  1. 节点的度:某个节点的度等于该节点子节点的数量
  2. 树的度:一棵树中,最大的节点的度为该树的度
  3. 树的层:从根节点开始,根为一层,根的子节点为二层,以此类推
  4. 树的深度(高度):树的最大层次
  5. 叶子节点:度为0的节点称为叶子节点
  6. 分支节点:非叶子节点
  7. 子节点、父节点:如果一个节点A指向节点B,那么A是B的父节点,B是A的子节点
  8. 兄弟节点:如果两个节点有共同的父节点,那么这两个节点互为兄弟节点
  9. 祖先节点:一个节点的祖先节点,是从根节点到该节点本身经过的所有节点
  10. 后代节点:如果A是B的祖先节点,那么B是A的后代节点

获取树的深度

通过递归遍历数的各个节点,节点的深度为1+子节点的深度,如果子节点有过个,取最大的子节点的深度,如果节点没有子节点,直接返回1
注意如果子节点存在,才去递归子节点

function getDeep(root) {
    if (!root) return 0
    if (!root.left && !root.right) return 1
    return 1 + Math.max(root.left ? getDeep(root.left) : 0, root.right ? getDeep(root.right) : 0)
}

树的遍历

树的遍历分为前序遍历,中序遍历与后续遍历
前序遍历即根节点在前,然后是左子节点,右子节点,
中序遍历即左子节点在前,然后是根节点,右子节点,
后序遍历即左子节点在前,然后是右子节点,根节点。

function loopFront(root) {
    console.log(root.value);
    root.left && loopFront(root.left)
    root.right && loopFront(root.right)
}
function loopMid(root) {
    root.left && loopFront(root.left)
    console.log(root.value);
    root.right && loopFront(root.right)
}
function loopBack(root) {
    root.left && loopFront(root.left)
    root.right && loopFront(root.right)
    console.log(root.value);
}

通过前中遍历还原二叉树

只有前中,中后遍历可还原二叉树,核心思想是递归
通过找出跟节点的位置找到左子树的前中遍历和右子树的前中遍历
递归左子树与右子树

// 根据两个遍历还原数
function restoreByFrontMid(front, mid) {
    if (front.length != mid.length) return null
    if (front.length == mid.length && mid.length == 0) return null
    // 根节点
    let root = front[0];
    // 根节点在中序遍历中的位置
    let midRootIndex = mid.indexOf(root);
    // 左子树前序遍历
    let leftFront = front.substr(1, midRootIndex);
    // 左子树中序遍历
    let leftMid = mid.substr(0, midRootIndex);
    // 右子树前序遍历
    let rightFront = front.substr(midRootIndex + 1);
    // 右子树中序遍历
    let rightMid = mid.substr(midRootIndex + 1);
    let node = new Node(root);
    // console.log(leftFront, leftMid)
    node.left = restoreByFrontMid(leftFront, leftMid)
    node.right = restoreByFrontMid(rightFront, rightMid)
    return node
}

树的搜索

深度优先

原理类似前序遍历,先搜索自己,如果子节不是要找的节点,就搜索左子节点,左子节点全部搜完才去搜右子节点

// 深搜
function deepFirstSearch(root, target) {
    console.log(root.value)
    if (!root) return false;
    if (root.value == target) return true;
    return root.left && deepFirstSearch(root.left, target) || root.right && deepFirstSearch(root.right, target)
}

广搜

分治法,搜索一个数组内的节点,最初是一个只包含根节点的数组,如果这个数组内的节点都不是要找的,那么把这些节点的子节点放入一个新数组(如果子节点存在的话),重新搜索这个节点数组

// 广搜
function rangeFirstSearch(root, target) {
    if (!root) return false;
    function _rangeFirstSearch(arr, target) {
        let arr_ = [];
        for (let i = 0; i < arr.length; i++) {
            console.log(arr[i].value)
            if (arr[i].value == target) return true;
            if (arr[i].left) arr_.push(arr[i].left)
            if (arr[i].right) arr_.push(arr[i].right)
        }
        if (arr_.length > 0) return _rangeFirstSearch(arr_, target)
        else return false
    }
    return _rangeFirstSearch([root], target)
}

最小生成树

普利姆算法

普里姆算法是以一个节点开始,遍历它的相邻节点,找出代价最小的节点,然后遍历这两个节点的相邻接点,找出下一个代价最小节点
这个算法需要一个数组来保存已经连接的节点,并且在遍历相邻节点时,不考虑已遍历节点,当以连接节点的数组长度等于总节点长度时,完成并返回任一节点

// max
const max = 10000;
// 创建各个点
let a = new Node('A');
let b = new Node('B');
let c = new Node('C');
let d = new Node('D');
let e = new Node('E');
// 点集合
let pointSet = [a,b,c,d,e];
// 边集合
let distance = [
  [0, 4, 7, max, max],
  [4, 0, 8, 6, max],
  [7, 8, 0, 5, max],
  [max, 6, 5, 0, 7],
  [max, max, max, 7, 0]
];
function Node(val) {
  this.value = val;
  this.neighbor = [];
}
// 普利姆算法
function prim(pointSet, distance) {
    let nodes = [pointSet[0]];
    while (nodes.length < pointSet.length) {
        let newPoint = getMinPoint(pointSet, distance, nodes);
        nodes.push(newPoint);
    }
    function getMinPoint(pointSet, distance, nodes) {
        let fromNode = null;
        let endNode = null;
        let min = max;
        for (let i = 0; i < nodes.length; i++) {
            let nowPonitIndex = pointSet.indexOf(nodes[i]);
            for (let j = 0; j < distance[nowPonitIndex].length; j++) {
                if (distance[nowPonitIndex][j] < min && !nodes.includes(pointSet[j])) {
                    fromNode = nodes[i];
                    endNode = pointSet[j];
                    min = distance[nowPonitIndex][j]
                }
            }
        }
        fromNode.neighbor.push(endNode)
        endNode.neighbor.push(fromNode)
        return endNode
    }
    return nodes[0]
}

克鲁斯卡尔算法

克鲁斯卡尔算法是一直找代价最小边,并连接两个节点
克鲁斯卡尔算法将已经连接的点作为一个部落(数组)放在一个部落列表里,通过这个部落列表判断能不能连接以及怎么连接
克鲁斯卡尔算法解决的是两个问题

  1. 能不能连
  2. 怎么连
    关于能不能连,如果两个节点的代价大于0,小于max,并且不在一个部落,就可以连
    (一个在部落,一个不在部落,两个在不同部落,都可以连接)
    关于怎么连接,如果两个节点都不在部落,那么增加一个新部落,里面是两个节点,并将新部落存入部落列表
    如果一个在部落,一个不在部落,将不在部落的节点存入另一个节点的部落
    如果两个节点在相同部落,那么将两个部落合并
// max
const max = 10000;
// 创建各个点
let a = new Node('A');
let b = new Node('B');
let c = new Node('C');
let d = new Node('D');
let e = new Node('E');
// 点集合
let pointSet = [a,b,c,d,e];
// 边集合
let distance = [
  [0, 4, 7, max, max],
  [4, 0, 8, 6, max],
  [7, 8, 0, 5, max],
  [max, 6, 5, 0, 7],
  [max, max, max, 7, 0]
];
function Node(val) {
  this.value = val;
  this.neighbor = [];
}
// 能不能连
function canLink(clanArr, fromPoint, endPoint) {
    let fromIn = null;
    let endIn = null;
    for (let i = 0; i < clanArr.length; i++) {
        if (clanArr[i].includes(fromPoint)) {
            fromIn = clanArr[i]
        }
        if (clanArr[i].includes(endPoint)) {
            endIn = clanArr[i]
        }
    }
    if (fromIn && endIn && fromIn == endIn) {
        return false
    }
    return true
}
// 连接两个点
function link(clanArr, fromPoint, endPoint) {
    fromPoint.neighbor.push(endPoint);
    endPoint.neighbor.push(fromPoint);
    let fromIn = null;
    let endIn = null;
    for (let i = 0; i < clanArr.length; i++) {
        if (clanArr[i].includes(fromPoint)) {
            fromIn = clanArr[i]
        }
        if (clanArr[i].includes(endPoint)) {
            endIn = clanArr[i]
        }
    }
    if (!fromIn && !endIn) { // 两个点都不在任何部落
        clanArr.push([fromPoint, endPoint])
    } else if (fromIn && !endIn) { // fromPoint在部落,endPoint不在部落
        fromIn.push(endIn)
    } else if (!fromIn && endIn) { // endPoint在部落,fromPoint不在部落
        endIn.push(fromPoint)
    } else if (fromIn && endIn && fromIn != endIn) { // 两个点在不同部落
        fromIndex = clanArr.indexOf(fromIn);
        clanArr[fromIndex] = fromIn.concat(endIn);
        endIndex = clanArr.indexOf(endIn);
        clanArr.splice(endIndex, 1)
    }
}
// 克鲁斯卡尔算法
function kruskal(pointSet, distance) {
    let clanArr = [];
    while (true) {
        let fromPoint = null;
        let endPoint = null;
        let min = max;
        for (let i = 0,len = distance.length; i < len; i++) {
            for (let j = 0,len = distance[i].length; j < len; j++) {
                if (distance[i][j] != 0 && distance[i][j] < min && canLink(clanArr, pointSet[i], pointSet[j])) {
                    fromPoint = pointSet[i];
                    endPoint = pointSet[j];
                    min = distance[i][j];
                }
            }
        }
        link(clanArr, fromPoint, endPoint);
        if (clanArr.length == 1 && clanArr[0].length == pointSet.length) {
            break
        }
    }
}
kruskal(pointSet, distance)

贪心算法

当遇到一个求全局最优解的问题时,如果可将全局问题切分成小的局部问题,并寻求局部最优解,同时可以证明局部最优解的累计结果就是全局最优解,则可以使用贪心算法

例:找零问题

假设你有一间小店,需要找给客户46分钱的硬币,你的货柜里面只有面额为25分,10分,5分,1分的硬币,如何找零才能确保数额正确并且硬币数最少

function zhaoling(target, arr) {
    console.log(1)
    if (target == 0 && arr.length == 0) {
        return []
    }
    let result = [];
    while (target > 0) {
        let money = arr.filter((item) => item <= target)[0];
        if (money) {
            result.push(money);
            target -= money
        } else {
            return false
        }
    }
    return result
}
console.log(zhaoling(46, [200, 100, 50, 25, 10, 5, 1]))
// 25 10 10 1

在这个算法里面,每次都是找小于未找金额的硬币中最大的那个,但是这种寻求局部最优解的和未必是全局最优解

例如,找零41,零钱有[25, 20, 5, 1]贪心算法得出结果为[25, 5, 5, 5, 1],但是这明显不是最优解,最优解为[20, 20, 1]

但是由于贪心算法只需要考虑眼下的事,所以贪心算法效率较高。当你不需要必须是最优解的话,贪心算法是一个不错的选择

但是如果必须最优解的话,可以优化

function zhaoling(target, arr) {
    console.log(1)
    if (target == 0 && arr.length == 0) {
        return []
    }
    let result = [];
    while (target > 0) {
        let money = arr.filter((item) => item <= target)[0];
        if (money) {
            result.push(money);
            target -= money
        } else {
            return false
        }
    }
    return result
}

function perfectZhaoling(target, arr) {
    if (arr.length == 0) return false;
    if (target == arr[0]) return false;
    arr = arr.filter((item) => item <= target)
    let result = [];
    result = zhaoling(target, arr);
    // console.log(result)
    if (result == false) return false;
    for (let i = 1; i < arr.length - 1; i++) {
        let subResult = zhaoling(target, arr.slice(i));
        if (typeof subResult == 'object' && subResult.length < result.length) {
            result = subResult
        }
    }
    return result
}
console.log(perfectZhaoling(41, [200, 100, 50, 25, 20, 5, 1]))

此算法的原理是,先使用之前的贪心算法计算零钱为[200, 100, 50, 25, 20, 5, 1]的解,然后去掉最大的零钱,计算[100, 50, 25, 20, 5, 1]的解,如果得到的解的长度小于之前的解,那么就替换之前的解

动态规划

个人理解:(自己对动态规划的理解,可能不是很准确,仅供参考)
动态规划就是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。
与分治法不同的是,动态规划是将每个子问题的结果缓存起来,下次遇到相同子问题直接取缓存
在我的理解中,动态规划等于递归加缓存

例题1:青蛙跳台阶

一只青蛙一次只能跳一级或者两级台阶,那么这只青蛙跳上n级台阶有几种跳法?

let arr = []
function frog(n) {
    if (n <= 0) return false;
    if (n <= 3) return n;
    let result = frog(n - 1) + frog(n - 2);
    arr[n] = result
    return result
}
console.log(frog(10))

这个算法中arr即为缓存。

例题2:变态青蛙跳台阶

一只青蛙一次只能跳一级到n级台阶,那么这只青蛙跳上n级台阶有几种跳法?

function BTfrog(n) {
    if (n <= 0) return false;
    if (n <= 2) return n;
    let result = 0;
    for (let i = 1; i < n; i++) {
        result += BTfrog(i)
    }
    arr[n] = result
    return result
}
console.log(BTfrog(10))

例题3:最长公共子序列

有时候,我们需要比较两个字符串的相似程度,通常就是比较两个字符串有多少相同的公共子序列,例如有两个字符串‘asefhtbrf’,‘asmiiimhfoo’,以上两个字符串的最长公共子序列的ashf

思路:如果这两个字符串第一位相同,那么就返回第一位加LCS(str1.substr(1), str2.substr(1)),如果第一位不一样就返回LCS(str1, str2.substr(1))和LCS(str1.substr(1), str2)中比较长的一个

function LCS(str1, str2) {
    if (!str1 || !str2) return '';
    if (str1[0] == str2[0]) {
        return str1[0] + LCS(str1.substr(1), str2.substr(1));
    } else {
        let s1 = LCS(str1.substr(1), str2);
        let s2 = LCS(str1, str2.substr(1));
        return s1 > s2 ? s1 : s2;
    }
}

但是这样会产生很大的运算量,因为有很多重复的运算
我们可以加一个缓存来取消重复运算

let arr = []
function LCS(str1, str2) {
    if (!str1 || !str2) return '';
    // 先看有没有缓存
    for (let i = 0; i < arr.length; i++) {
        if (arr[i].str1 == str1 && arr[i].str2 == str2) {
            return arr[i].result
        }
    }
    let s;
    if (str1[0] == str2[0]) {
        s = str1[0] + LCS(str1.substr(1), str2.substr(1));
    } else {
        let s1 = LCS(str1.substr(1), str2);
        let s2 = LCS(str1, str2.substr(1));
        s = s1 > s2 ? s1 : s2;
    }
    // 将本次计算结果缓存起来
    arr.push({
        str1,
        str2,
        result: s
    })
    return s
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值