JavaScript 版数据结构与算法


简介

数据结构与算法:

  • 数据结构:计算机存储、组织数据的方式
  • 算法:一系列解决问题的清晰指令
  • 程序 = 数据结构 + 算法
  • 数据结构为算法提供服务,算法围绕数据结构进行操作

LeetCode 是什么:

  • LeetCode 官网:https://leetcode-cn.com
  • LeetCode 是一个专注于程序员技术成长和企业技术人才服务的品牌
  • 功能:题库、社区、竞赛、模拟面试等等

一、时间 / 空间复杂度计算

1、时间复杂度计算

  • 一个函数,用大 O 表示,比如 O(1)、O(n)、O(logN)、O(n^2) ……
  • 定性描述该算法的运行时间

2、空间复杂度计算

  • 一个函数,用大 O 表示,比如 O(1)、O(n)、O(n^2) ……
  • 算法在运行过程中临时占用存储空间大小的度量

二、数据结构

1、栈(Stack)

栈是什么:

  • 一个 先进后出 的数据结构
  • JavaScript 中没有栈,但可以用 Array 实现栈的所有功能
  • push 和 pop 实现 入栈 和 出栈
  • 或者使用:unshift 和 shift 实现 入栈 和 出栈

栈的应用场景:

  • 需要 先进后出 的场景
  • 例如:十进制转二进制、判断字符串的括号是否有效、函数调用堆栈……

判断字符串的括号是否有效(leetcode:20):

  • 越靠后的左括号,对应的右括号越靠前
  • 左括号入栈,右括号出栈,最后栈空了就是合法的
const isValid = function( str ) {
	// 如果字符串长度为奇数,直接可得不合法
	if(str.length % 2 === 1) return false;
	
	const stack = [];
	for(let i = 0; i < str.length; i++) {
		const a = str[i];
		if(a === '(' || a === '[' || a === '{') {
			stack.push(a);
		} else {
			const top = stack[stack.length - 1]
			if((top === '(' && a === ')') || (top === '[' && a === ']') || (top === '{' && a === '}') ) {
				stack.pop();
			} else {
				return false;
			}
		}
	}
	return stack.length === 0;
}

2、队列(Queue)

队列是什么:

  • 一个 先进先出 的数据结构
  • JavaScript 中没有队列,但可以用 Array 实现队列的所有功能
  • push 和 shift 实现队列的 入队 和 出队
  • 或者使用:unshift 和 pop 实现队列的 入队 和 出队

队列的使用场景:

  • 需要 先进先出 的场景
  • 例如:JS 异步中的任务队列、计算最近请求次数……

JS 异步中的任务队列:

  • JS 是单线程,无法同时处理异步中的并发任务
  • 使用任务队列先后处理异步任务
setTinmeout(() => {
	console.log(1)
}, 0)
console.log(2)

// 输出结果为: 
2
1

最近请求次数(leetcode:933):

  • 有新请求就入队,3000ms 前发出的请求出对
  • 队列的长度就是最近请求次数
var RecentCounter = function() {
	this.queue = [];
}

RecentCounter.prototype.ping = function(t) {
	this.queue.push(t);
	while(this.queue[0] < t - 3000) {
		this.queue.shift();
	}
	return this.queue.length;
}

3、链表(LinkedList)

链表是什么:

  • 多个元素组成的列表
  • 元素存储不连续,用 next 指针连在一起
  • JavaScript 中没有链表这种数据结构,可以用 Object 模拟链表

数组和链表的区别:

  • 数组:增删非首尾元素时往往需要移动元素
  • 链表:增删非首尾元素,不需要移动元素,只需要更改 next 的指向即可

操作链表:

const a = { val: 'a' }
const b = { val: 'b' }
const c = { val: 'c' }
const d = { val: 'd' }
a.next = b;
b.next = c;
c.next = d;

// 遍历链表
let p = a;
while(p) {
	console.log(p.val)
	p = p.next;
}

// 在链表中插入新值: 在 c 和 d 之间插入 e
const e = { val: 'e' }
c.next = e;
e.next = d;

// 删除
c.next = d

删除链表中的节点(leetcode:237):

  • 编写一个函数,使其可以删除某个链表中给定的非末尾节点,只会给被删除的节点
  • 无法直接获取被删除节点的上一个节点
  • 将被删除节点转移到下一个节点
  • 思路:将被删节点的值改为下一个节点的值
  • 删除下一个节点
var deleteNode = function(node) {
	node.val = node.next.val;
	node.next = node.next.next;
}

反转链表(leetcode:206):

  • 输入:1 -> 2 -> 3 -> 4 -> 5 -> null
  • 输出:5 -> 4 -> 3 -> 2 -> 1 -> null
  • 解题思路:反转两个节点:将 n+1 的 next 指向 n
  • 反转多个节点:双指针遍历链表,重复上述操作
var reverseLinkedList = function(head) {
	let p1 = head;
	let p2 = null;
	while(p1) {
		const tmp = p1.next;
		p1.next = p2;
		p2 = p1;
		p1 = tmp;
	}
	return p2;
}

两数相加(leetcode:2):

  • 解题步骤:新建一个空链表
  • 遍历被相加的两个链表,模拟相加操作,将“个位数”追加到新链表上,将“十位数”留到下一位去相加
var addTwoNumbers = function(l1, l2) {
	const l3 = new ListNode(0);
	let p1 = l1;
	let p2 = l2;
	let p3 = l3;
	let carry = 0;
	while(p1 || p2) {
		const v1 = p1 ? p1.val : 0;
		const v2 = p2 ? p2.val : 0;
		const val = v1 + v2;
		carry = Math.floor(val / 10);
		p3.next = new ListNode(val % 10);
		if(p1) p1 = p1.next;
		if(p2) p2 = p2.next;
		p3 = p3.next;
	}
	if(carry) {
		p3.next = new ListNode(carry);
	}
	return l3.next;
}

删除排序链表中的重复元素(LeetCode:83):

  • 解题思路:因为链表是有序的,所以重复元素一定相邻
  • 遍历链表,如果发现当前元素和下一个元素值相同,就删除下一个元素值
var deleteDuplicates = function(head) {
	let p = head;
	while(p && p.next) {
		if(p.val === p.next.val) {
			p,next = p.next.next;
		} else {
			p = p.next;
		}
	}
	return head;
}

判断链表是否有环(LeetCode:141):

  • 解题思路:用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表就有环
var hasCycle = function(head) {
	// 慢的指针
	let p1 = head;
	// 快的指针
	let p2 = head;
	while(p1 && p2 && p2.next) {
		p1 = p1.next;
		p2 = p2.next.next;
		if(p1 === p2) {
			return true;
		}
	}
	return false;
}

4、集合(Set)

集合是什么:

  • 一种 无序且唯一 的数据结构(无序不可重复)
  • ES6 中有集合,名为 Set
  • 集合的常用操作:去重、判断某元素是否在集合中、求交集……
// 去重
const arr = [1,1,2,3,4,4,5];
const arr2 = [...new Set(arr)];

// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(1);   
console.log(has);   // true

// 求交集
const set1 = new Set([2,3,6]);
const set3 = new Set([...set].filter(item => {
	set2.has(item);
}));

Set 操作:

  • 使用 Set 对象:new、add、delete、has、size
  • 迭代 Set:多种迭代方法、Set 与 Array 互转、求交集 / 差集
// new
let mySet = new Set();

// add
mySet.add(1);
mySet.add(2);
mySet.add(2);
mySet.add('hello world');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });

// has
const has = mySet.has(1);  // true
const has2 = mySet.has(3);  // false

// delete
mySet.delete(2);

// size
const size = mySet.size;

// 迭代
for(let item of mySet) {
	console.log(item)
}
// 或
for(let item of mySet.keys()) {
	console.log(item)
}
// 或
for(let item of mySet.values()) {
	console.log(item)
}

// Set 转为 Array
const arr = [...mySet];
const arr2 = Array.from(set);

// Array 转为 Set
const set = new Set([1,3,5]);

// 交集 intersection
const intersection = new Set([...mySet].filter(x => set.has(x)));

// 差集 difference
const difference = new Set([...mySet].filter(x => !set.has(x)))

两个数组的交集(LeetCode:349):

  • 解题步骤:用集合对 nums1 去重
  • 遍历 nums1,筛选出 nums2 也包含的值
var intersection = function(nums1, nums2) {
	return [...new Set(nums1)].filter(n => {
		// new Set(nums2).has(n);
		// 或者
		nums2.includes(n);
	})
}

5、字典(Map)

字典是什么:

  • 与集合类似,字典也是一种存储唯一值的数据结构,但它是以 键值对 的形式来存储的
  • ES6 中有字典,名为 Map
  • 字典的常用操作:键值对的增、删、改、查
// 实例化
const map = new Map();

// 增
map.set('a','aa');
map.set('b', 'bb');

// 查
map.get('a');
map.get('b');

// 删
map.delete('b');
// 清空
map.clear();

// 改:就是覆盖
map.set('a', 'aaaaa');

两个数组的交集(LeetCode:349):

  • 解题步骤:新建一个字典,遍历 nums1 ,填充字典
  • 遍历 nums2,遇到字典里的值就选出,并从字典中删除
var intersection = function(nums1, nums2) {
	const map = new Map();
	nums1.forEach(n => {
		map.set(n, true);
	});
	const res = [];
	nums2.forEach(n => {
		if(map.get(n)) {
			res.push(n);
			map.delete(n);
		}
	});
	return res;
}

判断字符串的括号是否有效(leetcode:20):

  • 越靠后的左括号,对应的右括号越靠前
  • 左括号入栈,右括号出栈,最后栈空了就是合法的
const isValid = function( str ) {
	// 如果字符串长度为奇数,直接可得不合法
	if(str.length % 2 === 1) return false;
	
	const stack = [];
	const map = new Map();
	map.set('(', ')');
	map.set('[', ']');
	map.set('{', '}');
	
	for(let i = 0; i < str.length; i++) {
		const a = str[i];
		if(map.has(a)) {
			stack.push(a);
		} else {
			const top = stack[stack.length - 1]
			if(map.get(top) === a) {
				stack.pop();
			} else {
				return false;
			}
		}
	}
	return stack.length === 0;
}

两数之和(LeetCode:1):

var twoSum = function(nums, target) {
	const map = new Map();
	for(let i = 0; i < nums.length; i++) {
		const n1 = nums[i];
		const n2 = target - n;
		if(map.has(n2)) {
			return [map.get(n2), i];
		} else {
			map.set(n1, i);
		}
	}
}

无重复字符的最长字串(LeetCode:3):

  • 用双指针维护一个滑动窗口,用来剪切字串
  • 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位
  • 过程中,记录所有窗口的长度,并返回最大值
var lengthOfLongSubstring = function(str) {
	let p = 0;
	let count = 0;
	const map = new Map();
	for(let i = 0; i < str.length; i += 1) {
		if(map.has(str[i]) && map.get(str[i] >= p)) {
			p = map.get(str[i] + 1);
		}
		count = Math.max(count, i - p + 1);
		map.set(str[i], i);
	}
	return count;
}

最小覆盖字串(LeetCode:76):

  • 解题步骤:用双指针维护一个滑动窗口
  • 移动右指针,找到包含 t 的字串,移动左指针,尽量减少包含 t 的字串的长度
  • 循环上述过程,找到包含 t 的最小字串
var minWindow = function(s, t) {
	let l = 0;
	let r = 0;
	const map = new Map();
	for(let item of t) {
		map.set(item, map.has(item) ? map.get(item + 1) : 1);
	}
	let needType = map.size;
	let res = '';
	while(r < s.length) {
	    const c = s[r];
	    if(map.has(c)) {
	    	map.set(c, map.get(c) - 1);
	    	if(map.get(c) === 0) {
	    		needType -= 1;
	    	}
	    }
	    while(needType) {
	    	const newRes = s.substring(l , r + 1);
	    	if(!res || newRes.length <= res.length) {
	    		res = newRes;
	    	}
	    	const c2 = s[l];
	    	if(map.has(c2)) {
	    		map.set(c2, map.get(c2) + 1);
	    		if(map.get(c2) === 1) {
	    			needType += 1;
	    		}
	    	}
	    	l += 1;
	    }
		r += 1;
	}
	return res;
}

6、树(Tree)

7、图()

8、堆()

三、进阶算法之 “搜索与排序”

1、排序和搜索简介

排序和搜索是什么:

  • 排序:把某个乱序的数据结构按照某种顺序进行排序,例如:数组变成升序或降序的数组(sort() 方法)
  • 搜索:通过某个关键字进行查找,例如:找出数组中某个元素的下标(indexOf() 方法)

排序算法:

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 归并排序
  • 快速排序
  • ……

搜索算法:

  • 顺序搜索
  • 二分搜索
  • ……

2、冒泡排序

  • 冒泡排序的思路:

比较所有相邻元素,如果第一个比第二个大,则交换;一轮下来,可以保证最后一个数是最大的
执行 n - 1 轮,就可以完成排序

  • 时间复杂度:使用两个嵌套循环,时间复杂度为 O(n^2)
const arr = [1, 3, 2, 8, 7, 3, 5, 1, 4]

// 冒泡排序
function bubbleSort(arr) {
    for (let i = 0; i < arr.length - 1; i++) {
        for (let j = 0; j < arr.length - i; j++) {
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
    return arr
}


Array.prototype.bubbleSort = function () {
    let arr = this
    for (let i = 0; i < arr.length - 1; i++) {
        for (let j = 0; j < arr.length - i; j++) {
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
    return this
}

console.log(bubbleSort(arr))  //原数组已改变 [ 1, 1, 2, 3, 3, 4, 5, 7, 8]

console.log("打印原数组=>", arr) // [ 1, 1, 2, 3, 3, 4, 5, 7, 8]

arr.bubbleSort()
console.log(arr) // 原数组已改变  [ 1, 1, 2, 3, 3, 4, 5, 7, 8]

3、选择排序

4、插入排序

5、归并排序

6、快速排序

  • 快速排序的思路:

分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面
递归:使用递归的方式对基准前后的子数组进行分区
退出条件:当子数组的长度小于等于1时结束递归

  • 时间复杂度:分区的时间复杂度是 O(n),递归的时间复杂度是 O(logN),所以快速排序的时间复杂度为 O(n * logN)
const arr = [1, 3, 2, 8, 7, 3, 5, 1, 4]

// 快速排序
function quickSort(arr) {
    if (arr.length <= 1) return arr
    let left = []
    let right = []
    let mid = arr[0]
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < mid) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    left = quickSort(left)
    right = quickSort(right)
    left.push(mid)
    return left.concat(right)
}

Array.prototype.quickSort = function () {
    const sort = (arr) => {
        if (arr.length <= 1) return arr
        let left = []
        let right = []
        let mid = arr[0]
        for (let i = 1; i < arr.length; i++) {
            if (arr[i] < mid) {
                left.push(arr[i])
            } else {
                right.push(arr[i])
            }
        }
        left = quickSort(left)
        right = quickSort(right)
        left.push(mid)
        return left.concat(right)
    }
    const result = sort(this)
    result.forEach((item, index) => this[index] = item)
}

console.log(quickSort(arr))  // 有序,不改变原数组 [1, 1, 2, 3, 3, 4, 5, 7, 8]

console.log("打印原数组=>", arr) // [1, 3, 2, 8, 7, 3, 5, 1, 4]

arr.quickSort()
console.log(arr)  // 有序,原数组已更改 [1, 1, 2, 3, 3, 4, 5, 7, 8]

7、顺序搜索

8、二分搜索

  • 二分查找的思路:

从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束,返回中间元素的下标
如果目标值大于或小于中间元素,则在大于或小于中间元素的那一半数组中搜索
如果目标值不在数组中,则返回 -1

  • 时间复杂度:每次比较都使搜索范围缩小一半,时间复杂度为 O(logN)
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]

// 二分查找
function binarySearch(arr, target) {
    let left = 0
    let right = arr.length - 1
    while (left <= right) {
        let mid = Math.floor((right + left) / 2)
        let ele = arr[mid]
        if (ele < target) {
            left = mid + 1
        } else if (ele > target) {
            right = mid - 1
        } else {
            return mid
        }
    }
    return -1
}

Array.prototype.binarySearch = function (target) {
    let arr = this
    let left = 0
    let right = arr.length - 1
    while (left <= right) {
        let mid = Math.floor((right + left) / 2)
        let ele = arr[mid]
        if (ele < target) {
            left = mid + 1
        } else if (ele > target) {
            right = mid - 1
        } else {
            return mid
        }
    }
    return -1
}

console.log(binarySearch(arr, 1))  // 0
console.log(arr.binarySearch(10))  // -1

9、LeetCode 算法题

四、算法之 “分而治之”

五、算法之 “动态规划”

六、算法之 “贪心算法”

七、算法之 “回溯算法”


总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值