数据结构与算法(一)
什么是数据结构与算法?
- 什么是数据结构和算法呢?
- 可能你之前经常在很多地方都看到有人讨论数据结构和算法,但是它到底是什么一直云里雾里.
- 因为似乎我们学习编程的过程中,没有必要了解这些,我只是在学习一门语言的基本语法/高级语法/做出界面效果/实现复杂的逻辑就可以了.
- 数据结构和算法?它是什么? l don’t care?
- 如果我们只是想了解语言的应用层面,那么数据结构和算法显得没有那么重要.口但是如果我们希望了解语言的设计层面,那么数据结构和算法就非常的重要.
- 举个例子:
Java的线性结构列表,有 ArrayList 和 LinkedList, 如果选择呢?
:死记住规则,你也可以很好的选择,但是如果你了解它们底层的数据结构就会非常清晰的知道如何选择.
到底什么是数据结构与算法? 官方没有定义…
民间定义:
- “数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相关的函数来给出。”—《数据结构、算法与应用》
- “数据结构是 ADT (抽象数据类型Abstract Data Type )的物理实现。”—《数据结构与算法分析》
- “数据结构( data structure )是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。”—中文维基百科
我们还是从自己的角度来认识数据结构吧:
- 数据结构就是在计算机中,存储和组织数据的方式
- 我们知道,计算机中数据量非常庞大,如何以高效的方式组织和存储呢?
- 这就好比一个庞大的图书馆中存放了大量的书籍,我们不仅仅要把书放进入,还应该在合适的时候能够取出来
常用的数据结构
队列(Queue)、树(Tree)、堆(Heap)、数组(ArrayList)、栈(Stack)、链表(Linked List)、图(Graph)、散列表(Hash)
- 常见的数据结构较多
- 每一种都有对应的应用场景,
不同的数据结构
和不同的操作
性能是不同的。 - 有的查询性能很快,有的插入速度很快,有的是插入头和尾的速度很快
- 有的做范围查找很快,有的允许元素重复,有的不允许重复等等
- 在开发中如何选择,要根据具体的需求来决定
- 每一种都有对应的应用场景,
- 注意:数据结构与语言无关,常见的编程语言,
直接或间接
的使用上述常见的数据结构 - 为什么学习JavaScript没有接触过数据结构? 好像只见过数组
- 单纯从
客户程序员
的角度,我们不需要过多的了解它们的实现细节 - 但是简单的使用不能让我们更加灵活地使用他们,了解真相,你才能获得真正的自由!
- 单纯从
什么是算法?
-
算法(Algorithm)的认识
- 在之前的学习中,我们可能学习过几种排序算法.并且知道,不同的算法,执行效率是不一样的.
- 也就是说解决问题的过程中,不仅仅数据的存储方式会影响效率,算法的优劣也会影响着效率。
- 那么到底什么是算法呢?
-
算法的定义:
- 一个有限指令集,没调指令的描述不依赖于语言
- 接受一些输入(有些情况不需要输入)
- 产生输出
- 一定在有限步骤之后终止
-
算法的通俗理解
- Algorithm 这个单词本意就是解决问题的办法/步骤逻辑。
- 数据结构的实现,离不开算法
数组结构
- JavaScript的数组就是 API的调用:不讲
- 普通其他语言的数组封装(比如Java的ArrayList)
- 常见的语言的数组
不能
存放不同的数据类型
,因此所有的在封装时通常存放在数组中的是Object类型 - 常见语言的数组容量不会自动改变(需要进行扩容操作)
- 常见语言
- 、’组进行中间插入和删除操作性能比较低
- 常见的语言的数组
栈结构
- 栈也是一种非常常见的数据结构,并且在程序中应用非常广泛。
- 数组:
- 我们知道数组是一种线性结构,并且可以在数组的任意位置插入和删除数组
- 但是有时候,我们为了实现某些功能,必须对这种任意性加以限制
- 而 栈和队列 就是比较常见的受限的线性结构,我们先来学习栈结构
- 栈(stack)它是一种受限的线性表,后进先出(LIFO)
- 其显示是仅允许在表的一端进行插入和删除运算,这一段被称为栈顶,相对的,把另一端成为栈底。
- LIFO(Last In First Out)表示就是后进先出的的元素,第一个弹出栈空间。
- 向一个栈插入新元素又被称作 进栈、入栈和压栈,它是把新元素放到栈顶元素的上面,是指成为新的栈顶元素;
- 从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素
- 程序中什么是使用栈实现的呢?
- 函数调用栈
- 我们知道函数之间的相互调用:A调用B,B调用C,C调用D;
- 那样在执行的过程中,会将A压入栈,A没有执行完,所以不会弹出栈
- 在A执行过程中调用了B,会将B压入到栈,这个时候B在栈顶,A在栈底。
- 如果这个时候B可以执行完,那个B会弹出栈,但是B没有执行完,他调用了C
- 所以C会压入栈,并且在栈顶,而C调用了D,D会压入到栈顶。
- 所以当前栈循序是:栈顶 A -> B -> C -> D;
- D执行完,弹出站 C-> B -> A 依次弹出栈。
- 所以我们有函数调用栈的称呼,就来自与他们内部的实现机制,(通过栈来实现的)
栈的面试题
答案:C
解析:并没有说是一次性全部入栈(一次性入栈只有一个答案:1 2 3 4 5 6)
A: 65进栈,5出栈,43进栈,43出栈,6出栈,2,1进栈,2出栈,1出栈
B: 654进栈,4出栈,5出栈,3进栈出栈,22进栈出栈,1进栈出栈,6出栈
D: 65432进栈,2出栈,3出栈,4出栈,1 进栈出栈,5出栈,6出栈
实现栈结构
- 栈常见有哪些操作呢?
- push(element): 添加一个新元素到栈顶位置
- pop(): 移除栈顶的元素,同事返回被移除的元素
- peek(): 返回栈顶的元素,不对栈做出任何修改(这个方法不会移除栈顶的元素,仅仅返回它)
- isEmpty(): 如果栈里面没有任何元素返回 true, 否则返回 false
- size() 返回栈里的元素个数,这个方法和数组的length 属性很类似
- toString() :将栈结构的内容以字符形式返回
// 封装的栈类
function Stack() {
// 栈中的属性
this.item = [];
// 栈中常见的操作
// 1. 将元素压入栈
Stack.prototype.push = function(element) {
this.item.push(element);
}
// 2、从栈中取出元素
Stack.prototype.pop = function() {
return this.item.pop();
}
// 3、查看一下栈顶元素
Stack.prototype.peek = function() {
return this.item[this.item.length - 1];
}
// 4、判断栈是否为空
Stack.prototype.isEmpty = function() {
return this.item.length === 0;
}
// 5、获取栈中元素的个数
Stack.prototype.size = function() {
return this.item.length
}
// 6、toString 方法
Stack.prototype.toString = function() {
return this.item.join(',');
}
}
let s = new Stack();
s.push(1)
s.push(2)
s.push(3)
s.push(4)
console.log(s.pop());
console.log(s.peek());
console.log(s.isEmpty());
console.log(s.size());
console.log(s.toString());
十进制转二进制
- 为什么需要十进制转二进制?
- 现实生活中,我们主要使用十进制。
- 但在计算科学中,二进制非常重要,因为计算机里的所有内容
- 都是用二进制数字表示的(0和1)。
- 没有十进制和二进制相互转化的能力,与计算机交流就很困难。
- 如何实现十进制转二进制?
- 要把十进制转化成二进制,我们可以将该十进制数字和2整除,(二进制是满二进一),直到结果是0为止
// 函数:十进制转二进制
function desc2Bin(descNumber) {
// 1. 定义栈对象
let stack = new Stack();
// 2. 循环操作
while (descNumber > 0) {
// 2.1 获取玉树,并且压入栈
stack.push(descNumber % 2);
// 2.2 获取整除后的结果,作为下一次运行的数组
descNumber = Math.floor(descNumber / 2);
}
// 3. 从栈中去除 0 和 1
let fooBin = '';
while (!stack.isEmpty()) {
fooBin += stack.pop()
}
return fooBin;
}
// 测试 十进制转二进制的函数
console.log(desc2Bin(100));
console.log(desc2Bin(1000));
队列结构
-
受限的线性结构
- 我们已经学习了一种
受限的线性结构
:栈结构 - 并且知道这种受限的数据结构对于某些特定问题会有特定效果
- 我们已经学习了一种
-
队列(Queue),它是一种受限的线性表,先进先出(FIFO First in First Out)
- 受限之处在于它只允许再表的前端(front) 进行删除操作
- 而在表的后端(rear) 进行插入操作
-
打印队列:
- 有五份文档需要打印,这些文档会按照顺序放入到打印队列中
- 打印机会一次从队列中去除文档,优先放入的文档,优先被去除,并且对该文档进行打印
- 以此类推,知道队列中不在有新的文档
-
线程队列:
- 在开发中,为了让任务可以并行处理,通常会开启多个线程
- 但是,我们不能让大量的线程同时运行处理任务(占用过多的资源)
- 这个时候,如果有需要开启线程处理任务的情况,我们就会使用线程队列
- 线程队列会依照依序来启动线程,并且处理对应的任务。
如何实现队列?
-
队列的实现和栈一样,有两种方案
- 基于数组实现
- 基于链表实现
-
基于数组实现
-
队列有哪些常见的操作呢?
- enqueue(element):向队列尾部添加一个(或多个)新的项。
- dequeue()∶移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。
- front():返回队列中第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素只返回元素信息——与Stack类的peek方法非常类似)。
- isEmpty():如果队列中不包含任何元素,返回true,否则返回false。size():返回队列包含的元素个数,与数组的length属性类似。
- toString():将队列中的内容,转成字符串形式
- size() : 返回队列包含的元素
-
// 封装队列 function Queue() { // 属性 this.item = []; // 方法 Queue.prototype.enqueue = function(element) { this.item.push(element) } Queue.prototype.dequeue = function() { this.item.shift(); } Queue.prototype.front = function() { return this.item[0] } Queue.prototype.isEmpty = function () { return this.item.length === 0 } Queue.prototype.size = function () { return this.item.length } Queue.prototype.toString = function (partition) { return this.item.join(partition || ''); } } // 使用 let queue = new Queue(); queue.enqueue(11) queue.enqueue(22) queue.enqueue(33) queue.dequeue(); console.log(queue.size()); console.log(queue.isEmpty()); console.log(queue.front()); console.log(queue.toString(','));
队列面试题(击鼓传花)
// 面试题:击鼓传花
function passGame(nameList, number) {
// 创建一个队列
let queue = new Queue();
// 将所有人放入队列
nameList.forEach(element => {
queue.enqueue(element)
})
// 开始数数字
while (queue.size() > 1) {
// 不是 num 的时候,重新加入到队列的末尾
// 是 num 这个数字的时候,将其从队列中删除
for (let i = 0; i < number - 1; i++) {
queue.enqueue(queue.dequeue())
}
// num对应的这个人,直接从队列中删除
queue.dequeue();
}
// 获取剩下的人
return queue.front()
}
const names = [ 'Rookie', '姜晨露', '刘美丽']
console.log(passGame(names, 10));