作为一名开发者,无论是前端还是后端,哪能不知道常见的数据结构???数据结构是通用的,无论你是学js、java、python,基本上数据结构都是相通的,了解数据结构也让我们更容易学会其他编程语言!
好吧,我是菜鸟一枚,职业Ctrl+C、Ctrl+V
者,精通js、vue、react等单词的拼写,工作范围了解到的数据结构有限,这里记录下几种与我们息息相关的数据结构吧,后面再循循渐进!
数据结构与算法
栈
介绍
- 栈属于
受限的线性结构
,其特点为先进后出
常见操作
- push(element):添加一个新元素到栈顶位置;
- pop:移除栈顶的元素,同时返回被移除的元素;
- peek:返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它);
- isEmpty:如果栈里没有任何元素就返回true,否则返回false;
- size:返回栈里的元素个数。这个方法和数组的length属性类似;
- toString:将栈结构的内容以字符串的形式返回。
栈操作封装
js
// 栈
class Stack {
constructor(items = []) {
this.items = items
}
// 向栈内添加元素
push(element) {
this.items.push(element)
}
// 栈内弹出元素
pop() {
return this.items.pop()
}
// 返回栈顶元素
peek() {
const length = this.items.length
return this.items[length - 1]
}
// 判断栈是否为空
isEmpty() {
return this.items.length === 0
}
// 判断栈内元素个数
size() {
return this.items.length
}
// 转字符串
toString() {
let str = '';
for (let i of this.items) {
str = i + ' ' + str
}
return str
}
}
// 测试
// const stack = new Stack();
// stack.push('abc');
// stack.push('def');
// stack.push('ghi');
// console.log(stack.items);
// console.log(stack.pop())
// console.log(stack.items);
// console.log(stack.peek());
// console.log(stack.isEmpty());
// console.log(stack.size());
// console.log(stack.toString())
实战:十进制转二进制
js
// 实战:二进制转十进制
function dec2bin(dec) {
let stack = new Stack();
while (dec > 0) {
// 十进制除2取余,依次放入栈中
stack.push(dec % 2);
dec = Math.floor(dec / 2)
}
return stack.toString()
}
console.log(dec2bin(50))
队列
介绍
- 队列属于
受限的线性结构
,其特点为先进先出
常见操作
- enqueue(element):添加,向队列尾部添加一个(或多个)新的项;
- dequeue:删除,移除队列的第一(即排在队列最前面的)项,并返回被移除的元素;
- front:返回队列中的第一个元素
- isEmpty 判断是否为空
- size 返回队列长度
- toString 队列转字符串,(将队列已字符串的形式返回)
队列操作的封装
js
// 队列
class Queue {
constructor(items = []) {
this.items = items
}
// 添加
enqueue(element) {
// 从队尾添加
this.items.push(element)
}
//删除
dequeue() {
// 从队首删除
return this.items.shift()
}
// 返回第一个元素
front() {
return this.items[0]
}
//是否为空
isEmpty() {
return this.items.length === 0
}
//大小
size() {
return this.items.length
}
//转字符串
toString() {
let str = '';
for (let i of this.items) {
str += i + ' '
}
return str;
}
}
const queue = new Queue()
// 测试
queue.enqueue('a');
queue.enqueue('b');
queue.enqueue('c');
queue.enqueue('d');
console.log(queue.dequeue())
console.log(queue.front())
console.log(queue.isEmpty())
console.log(queue.size())
console.log(queue.toString())
队列实践
击鼓传花游戏:指定一个队列,和一个数字,列如数字3,每次将队列从前往后数3,当数到队列某个元素为3时,移除队列,找出一个元素!
js
// 击鼓传花 指定一个队列,和一个数字,列如数字3,每次将队列从前往后数3,当数到队列某个元素为3时,移除队列,找出一个元素
function passGame(nameList, number) {
let queue = new Queue(nameList)
while (queue.size() > 1) {
for (let i = 0; i < number - 1; i++) {
// 依次放入到队尾
queue.enqueue(queue.dequeue())
}
//符合number的,直接删除
queue.dequeue()
}
return queue.toString()
}
const winer = passGame(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3);
console.log(winer)
优先级队列封装
可以指定插入优先级,数字越小,优先级越高,放在队列越靠前
js
// 优先级队列
class PriorityQueue {
// 封装属性
items = []
// 内部类
enqueue(element) {
const { priority } = element;
// 判断队列是否为空
if (this.items.length === 0) {
this.items.push(element)
} else {
// 定义插入状态
let added = false;
for (let i = 0; i < this.items.length; i++) {
// 数字越小,优先级越高
if (priority < this.items[i].priority) {
this.items.splice(i, 0, element);
added = true;
// 找到后就停止循环
break;
}
}
// priority比所有都大 优先级最低 直接放到最后
if (!added) {
this.items.push(element);
}
}
}
//删除
dequeue() {
// 从队首删除
return this.items.shift()
}
// 返回第一个元素
front() {
return this.items[0]
}
//是否为空
isEmpty() {
return this.items.length === 0
}
//大小
size() {
return this.items.length
}
//转字符串
toString() {
let str = '';
for (let i of this.items) {
str += i.name + ' '
}
return str;
}
}
const priorityQueue = new PriorityQueue();
priorityQueue.enqueue({ name: 'Tom', priority: 1 });
priorityQueue.enqueue({ name: 'Rose', priority: 3 });
priorityQueue.enqueue({ name: 'Jack', priority: 2 });
priorityQueue.enqueue({ name: 'Duck', priority: 0 });
console.log(priorityQueue.toString());
单向链表
介绍
优点
- 链表的元素在内存中不必是连续的空间,可以充分利用计算机内存,实现灵活的内存动态管理
- 链表不必在创建时确定大小,并且可以无限延申下去
- 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多
缺点
- 链表访问任一元素,都需要从头还是查找(无法跳过第一个元素访问任意一个元素)
- 无法通过下标值直接访问元素,需要从头开始一个个访问,知道查找到对应元素
- 虽然轻松到达下一节点,但是返回到前一个节点困难
常见操作
- append(element):添加,向链表尾部添加原素
- insert(position,element):向链表的特定位置插入一个新的项
- get(position):获取对应位置的元素;
- indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;
- update(position,element):修改某个位置的元素;
- removeAt(position):从链表的特定位置移除一项;
- remove(element):从链表中移除一项;
- isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
- size():返回链表包含的元素个数,与数组的length属性类似;
- toString():由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;
单向链表封装
js
// 封装单向链表
class LinkedList {
// 内部类
Node(data) {
return {
data: data,
next: null
}
}
//头部
head = null;
length = 0;
// 插入(链表最后位置)
append(data) {
//1.创建新节点
let newNode = this.Node(data);
//2.添加节点
//情况1:链接为空时
if (this.length === 0) {
this.head = newNode;
} else {
//情况2:链表不为空
let current = this.head;
// 找到最后一个节点
while (current.next) {
current = current.next
}
// 将最后节点的next 指向新节点
current.next = newNode;
// 长度价1
}
this.length += 1
}
insert(data, position) {
//边界判定
if (position < 0 || position > this.length) {
return false
}
//1.创建新节点
let newNode = this.Node(data);
//2.插入新节点
if (position === 0) {
//2.1插入到首部
//让新节点的next 指向第一个节点
newNode.next = this.head;
//头部指向新节点
this.head = newNode;
} else {
let index = 0;
let prev = null;
let current = this.head;
while (index++ < position) {
// 往后移(prev变为当前节点 当前节点后移一位)(先把当前节点保存,再向后移,就可以知道链接的上一个节点了)
prev = current
current = current.next
}
// 切记先改 再保存(赋值原则) 否则 先赋值,后值又改了,赋值就不对了
newNode.next = current
prev.next = newNode;
}
this.length += 1;
return true
}
// 查找方法
get(position) {
//边界判断
if (position < 0 || position >= this.length) {
return null
}
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next;
}
return current.data;
}
// indexOf方法判断是否含有某个节点
indexOf(data) {
let current = this.head;
let index = 0;
// current不为null就一直向下查找 找不到就返回-1
while (current) {
if (current.data === data) {
return index
}
current = current.next;
index += 1
}
return -1
}
// 更新指定位置节点数据
update(position, newData) {
//越界判断
if (position < 0 || position >= this.length) {
return false
}
// 查找到当前节点
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next;
}
// 将节点数据修改
current.data = newData;
return true
}
// 删除指定位置节点数据
removeAt(position) {
//越界判断
if (position < 0 || position >= this.length) {
return false
}
//删除元素
if (position == 0) {
// 第一个节点 直接更改head的指向就行
this.head = this.head.next
} else {
// 找到指定位置节点
let current = this.head;
let index = 0;
let prev = null;
while (index++ < position) {
prev = current;
current = current.next;
}
previous.next = current.next
}
this.length -= 1;
return current.data;
}
// 删除数据
remove(data) {
let index = this.indexOf(data);
return this.removeAt(index);
}
// 查询是否未空
isEmpty(){
return this.length == 0
}
// 查询大小
size(){
return this.length
}
}
// 插入测试
// const linkedList = new LinkedList();
// linkedList.append('aaa');
// linkedList.append('bbb');
// linkedList.append('ccc');
// linkedList.append('ddd');
// linkedList.insert('eee',2);
// console.log(linkedList)
双向链表
介绍
- 双向链表:既可以从头遍历到尾,又可以从尾遍历到头。链接是双向的,实现原理是一个节点既有向前链接的引用,又有向后链接的引用
- 双向链表不仅有head指针指向第一个节点,又有tail指针指向最后一个节点
- 每一个节点都有prev、data、next3个部分组成
- 双向链表第一个节点的prev指向null,最后一个节点的next指向null
双向链表封装
关于删除节点:
关于插入节点:
js
// 封装双向链表
class DoubleLinklist {
Node(data) {
return {
data,
prev: null,
next: null
}
}
// 属性
head = null;
tail = null;
length = 0;
// append添加
append(data) {
let newNode = this.Node(data);
//判断链表是否为空
if (this.length == 0) {
// 为空直接头尾指向新节点
this.head = newNode;
this.tail = newNode;
} else {
// 当不是空时
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
this.length += 1;
}
// 向后遍历字符串
backwardString() {
let current = this.head;
let resultStr = '';
while (current) {
resultStr = resultStr + ' ' + current.data;
current = current.next;
}
return resultStr;
}
// 向前遍历成字符串
forwardString() {
let current = this.tail;
let resultStr = '';
while (current) {
resultStr = resultStr + ' ' + current.data;
current = current.prev;
}
return resultStr;
}
// 向指定位置插入数据
insert(position, data) {
// 边界判断
if (position < 0 || position > this.length) {
return false
}
// 创建节点
let newNode = this.Node(data);
// 1.原链表为空
if (this.length == 0) {
this.head = newNode;
this.tail = newNode;
} else {
// 2原链表不为空
if (position == 0) {
//2.1插入头部
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
} else if (position == this.length) {
//2.2插入尾部
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
} else {
//插入中间
// 找到position对应节点
let index = 0;
let current = this.head;
while (index++ < position) {
current = current.next;
}
current.prev.next = newNode;
newNode.prev = current.prev;
newNode.next = current;
current.prev = newNode;
}
}
this.length += 1;
}
// 获取数据
get(position) {
// 边界判断
if (position < 0 || position >= this.length) {
return null;
}
let current = null;
if (position < (this.length / 2)) {
// 当查找的值在前半段
current = this.head;
let index = 0;
if (index++ < position) {
current = current.next
}
} else {
// 当查找的值在后半段
current = this.tail;
let index = this.length - 1
while (index-- > position) {
current = current.prev;
}
}
return current.data;
}
// 找到数据的位置,没有返回-1
indexOf(data) {
//1.定义变量
let current = this.head
let index = 0
//2.遍历链表,查找与data相同的节点
while (current) {
if (current.data == data) {
return index
}
current = current.next
index += 1
}
return -1
}
// 更新
update(position, newData) {
// 边界判断
if (position < 0 || position >= this.length) {
return null;
}
let current = null;
if (position < (this.length / 2)) {
// 当查找的值在前半段
current = this.head;
let index = 0;
if (index++ < position) {
current = current.next
}
} else {
// 当查找的值在后半段
current = this.tail;
let index = this.length - 1
while (index-- > position) {
current = current.prev;
}
}
current.data = newData;
return true;
}
// 删除指定位置数据
removeAt(position) {
// 边界判断
if (position < 0 || position >= this.length) {
return false;
}
// 删除节点
if (this.length == 1) {
// 1.链表只有一个节点
this.head = null;
this.tail = null;
} else {
//2.链表不止一个节点
if (position == 0) {
//3.1删除第一个节点
this.head.next.prev = null;
this.head = this.head.next;
} else if (position == this.length - 1) {
//3.2删除最后一个节点
this.tail.prev.next = null;
this.tail = this.tail.prev;
} else {
//3.3删除中间的节点
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next;
}
current.next.prev = current.prev;
current.prev.next = current.next;
}
}
this.length -= 1
}
}
// // 测试
let list = new DoubleLinklist()
// //2.测试append方法
list.append('aaa')
list.append('bbb')
list.append('ccc')
// console.log(list);
// 3.测试输出字符串
// console.log(list.backwardString(),list.forwardString())
// 4.测试insert方法
list.insert(0, '000');
list.insert(4, '111');
list.insert(3, 'ddd')
// console.log(list)
// 5.测试get
// console.log(list.get(3))
// console.log(list.get(1))
// 6.测试removeAt
// console.log(list)
// list.removeAt(0);
// list.removeAt(4);
// list.removeAt(2)
// console.log(list)
以上的栈、队列、链表在我们日常学习源码中非常常见,列如vue、react
的源码中。
待进阶更新:哈希表、树、图结构