算法基础
导论
什么是程序?程序的本质就是解决问题,而解决问题的核心就是算法。对于程序员来说,算法能力可以真实反应你的逻辑思维与开发潜力。当然,算法能力不是与生俱来的,它是天才与积累的结合,算法能力的提升需要我们不断地进行练习、思考与总结。从今天开始,我会带着大家一起来学习算法,来领会编程的艺术。
工欲善其事必先利其器。首先,我们会先了解一些基础的数据结构:队列、栈、链表、二叉树,然后通过代码实现这些数据结构,从而深入理解这些基础的数据结构,为后续的挑战打下坚实的基础。话不多说,让我们开始吧!
队列
队列(queue)是一种先进先出(FIFO)的数据结构,就比如:我们排队结账,先来的人站在队列的头,后来的排在队列的尾。当开始结账时,先来的人先结,后来的人后结,这就是队列数据结构。让我们来试着用JS代码中的Array来模拟一个队列吧!
数组模拟队列
// 创建队列
const queue = [];
// 开始入队
queue.push('第1个人');
queue.push('第2个人');
queue.push('第3个人');
queue.push('第4个人');
// 开始结账
while(queue.length) {
// 当前队首
const top = queue[0];
// 处理队首
console.log(`${top}结完账`);
// 队首出队
queue.shift();
}
// 队空
queue;
栈
栈(stack)是一种后进先出(LIFO)的数据结构,它和队列恰好相反。比如:我们往箱子里放盒子,先放进去的盒子在栈的底部,后放进去的盒子在栈的顶部。当我们往外取盒子时,后放进去的盒子会被先取出来,就是栈数据结构。让我们来试着用JS代码中的Array来模拟一个栈吧!
数组模拟栈
// 创建栈
const stack = [];
// 开始入栈
stack.push('第1本书');
stack.push('第2本书');
stack.push('第3本书');
stack.push('第4本书');
// 开始往外取书
while(stack.length) {
// 当前栈顶
const top = stack[stack.length - 1];
// 取出栈顶的书
console.log(`${top}被取出来了`);
// 栈顶出栈
stack.pop();
}
// 栈空
stack;
链表
链表和数组相似,都是有序的列表,都是线性结构(有且仅有一个前驱,有且仅有一个后继)。不同点在于:链表中的单位叫做结点,节点的分布可以是离散的。
对象模拟链表
// 链表结点构造函数
function ListNode(val) {
this.val = val;
this.next = null;
}
// 创建链表结点
const node = new ListNode(1);
node.next = new ListNode(2);
// 链表元素的添加:在尾部添加4
node.next.next = new ListNode(4);
// 链表元素的添加:在2和4的中间插入3
const node3 = new ListNode(3);
node3.next = node.next.next;
node.next.next = node3;
// 链表元素的删除:删除结点3
node.next.next = node.next.next.next;
二叉树
树数据结构
树数据结构是一种类似于现实中树(tree)的数据结构,它有根节点、边、节点、叶子节点概念。在计算机中表现如下:
二叉树
二叉树是指满足以下要求的树:
-
它可以没有根结点,作为一棵空树存在
-
如果它不是空树,那么必须由根结点、左子树和右子树组成,且左右子树都是二叉树。
如下,就是一个二叉树:
注意: 二叉树不能被简单定义为每个结点的度都是2的树。普通的树并不会区分左子树和右子树,但在二叉树中,左右子树的位置是严格约定、不能交换的。对应到图上来看,也就意味着 B 和 C、D 和 E、F 和 G 是不能互换的。
对象模拟二叉树
// 二叉树 树节点构造函数
function TreeNode(val) {
this.val = val;
this.left = null;
this.right = null;
}
// 创建二叉树的根节点
const treeNodeA = new TreeNode('A');
// 填充二叉树
treeNodeA.left = new TreeNode('B');
treeNodeA.right = new TreeNode('C');
treeNodeA.left.left = new TreeNode('D');
treeNodeA.left.right = new TreeNode('E');
treeNodeA.right.left = new TreeNode('F');
treeNodeA.right.right = new TreeNode('G');
私货课堂
先提出一个问题,数组和链表相似,那我们什么时间用数组,什么时间用链表,它们的性能又有什么不同呢?
补充:二叉树的遍历算法
在面试中,二叉树的各种姿势的遍历,是非常容易作为独立命题点来考察的,而且这个考察的频率极高极高。所以专门挑出一个章节来讲二叉树的遍历。
二叉树的遍历是指:以一定的顺序规则,逐个访问二叉树的所有结点,这个过程就是二叉树的遍历。
按照顺序规则的不同,遍历方式有以下四种:
-
先序遍历
-
中序遍历
-
后序遍历
-
层次遍历 按照实现方式的不同,遍历方式又可以分为以下两种:
-
递归遍历(先、中、后序遍历)
-
迭代遍历(层次遍历)
其中先序遍历、中序遍历、后序遍历采用的都是递归的实现方式,所以我们可以举一反三,掌握其中一种即可推导出其余两种。而层次遍历的考察相对比较孤立,我们会把它放在后续的真题归纳解读环节来讲。
实现二叉树的递归遍历(DFS)
二叉树的先序遍历、中序遍历、后序遍历,我们采用的都是递归函数的实现方式。
递归函数是指:编程语言中,函数Func(Type a,……)直接或间接调用函数本身,则该函数称为递归函数。
我们引用上一节实现的二叉树来进行遍历:
// 二叉树 树节点构造函数
function TreeNode(val) {
this.val = val;
this.left = null;
this.right = null;
}
// 创建二叉树的根节点
const treeNodeA = new TreeNode('A');
// 填充二叉树
treeNodeA.left = new TreeNode('B');
treeNodeA.right = new TreeNode('C');
treeNodeA.left.left = new TreeNode('D');
treeNodeA.left.right = new TreeNode('E');
treeNodeA.right.left = new TreeNode('F');
treeNodeA.right.right = new TreeNode('G');
我们首先来实现二叉树的先序遍历:
// 二叉树的先序遍历
function preorder(root) {
if(!root) return;
console.log(`当前遍历的节点是:${root.val}`);
preorder(root.left);
preorder(root.right);
}
我们接着来实现二叉树的中序遍历:
// 二叉树的先序遍历
function preorder(root) {
if(!root) return;
preorder(root.left);
console.log(`当前遍历的节点是:${root.val}`);
preorder(root.right);
}
最后,我们接着来实现二叉树的后序遍历:
// 二叉树的先序遍历
function preorder(root) {
if(!root) return;
preorder(root.left);
preorder(root.right);
console.log(`当前遍历的节点是:${root.val}`);
}
总结
在这一节,我们详细介绍了队列、栈、链表、二叉树这几种基础数据结构,并通过js代码实现。接下来我们会继续介绍许多有趣的算法,比如贪心算法、双指针、DFS、BFS等,让我们一起来领略编程的乐趣吧!
我是何以庆余年,如果文章对你起到了帮助,希望可以点个赞,谢谢!
如有问题,欢迎在留言区一起讨论。