数据结构初理解
1.单项链表(es5)
逻辑:单向链表由头指针和节点组成。node节点保存一个数据,以及一个指向下一个节点的指针。
想要在链表中的到想要的数据,必须从头节点开始遍历,直到取得数据。
function Node(data){
this.data = data //得到传入的数据
this.next = null //默认指向空
}
//链表的属性
this.head = null //头节点指向空
this.length = 0 //初始长度为0
//链表的方法 在链尾增加一个节点
LinkList.prototype.append = function(data){
var newNode = new Node(data) //1.先创建一个节点
if(this.length ==0){ //根据链表长度是否为0,判断链表中是否有数据
this.head = newNode //没有其他节点 就直接将头指针指向的位置 放入这个新节点
}else{
var current = this.head //用一个新的指针 接受头指针 让这个新指针去找到最末端的节点
while(current.next){ //如果判断当前的节点,是否有下一个节点
current = current.next //有就指向下一个节点
}
current.next = newNode //当找到某个节点没有下一个节点 把这个新的节点作为它下一个节点
}
this.length +=1 //链表长度加一
}
LinkList.prototype.toString = function(){
var current = this.head //拿到头指针指向的节点
var outPut = ' '
while(current){ //判断这个节点是否存在 存在则执行完操作之后 指向下一个节点
outPut += current.data + ' '
current = current.next
}
return outPut
}
//插入方法 有两种情况在在头节点插入 ,和在非头节点插入 这里要借助两个相对位置节点来插入
LinkList.prototype.insert = function(position,data){ //position 代表要插入的位置
//先对position进行越界判断 不可能插入在负数的位置以及非链表长度内
if(position < 0 || position > this.length )
return false
let newnode = new Node(data) //创建新节点
if(position == 0){ //在链表第一个节点的位置插入
newnode.next = this.head
this.head = newnode
}
else{
let index = 0
let current = this.head //将current指向第一个节点
let previous = null //pre指向current的前一个位置 在这两个位置之间插入新节点
while(index < position){ //由于是链表结构 需要从头定位到position的位置 用index向下寻找
index += 1 //用while来移动参照物
previous = current //每次将current和previous都向后移动一个单位 直至current到达position的位置(初始index和current的都是在0的状态)
current = current.next
}
newnode.next = current //开始插入操作
previous.next = newnode
}
this.length +=1
return true
}
//链表其他操作
LinkList.prototype.XXX = function(position,data){
//有位置信息的 则要进行越界判断
//初始用current指向第一个节点 index从零开始
//利用while向后挪动指针 直到找到需要的位置 再进行想要的操作 (注意在头节点 可能要进行区分)
//返回要求的结果 (如果操作影响了长度,要注意修改长度)
}
}
//测试
let list = new LinkList()
list.append("1")
list.append("2")
list.append("3")
list.insert(2,"a")
alert(list) //alert默认执行了toString方法,
2.双向链表(es5)
双向链表中节点除了有
next指向下一个节点,还有prev指向前一个节点。双向链表的优点在于能够从头到尾迭代,也能够从尾到头迭代。如果从头到尾遍历到中间位置的时候,想反向从尾到头进行遍历,也是可以办到的。双向链表虽然比单向链表占了更多的内存,但是双向链表最大的好处是删除给定指针操作时不用再遍历一遍找到其
prev指向的节点,所以此时的删除操作单向链表时间复杂度是O(n),双向链表的时间复杂度是O(1)。
function DoubleLinkedList(){
//链表属性
this.length = 0
this.head = null
this.tail = null
//内部节点类
function Node(data){
this.pre = null
this.next = null
this.data = data
}
DoubleLinkedList.prototype.append = function(data){
let newnode = new Node(data)
if(this.length == 0){
this.head = newnode
this.tail = newnode
}else{
newnode.pre = this.tail
this.tail.next = newnode
this.tail = newnode
}
this.length += 1
}
DoubleLinkedList.prototype.toString = function(){ //转字符串
return this.backWardString()
}
DoubleLinkedList.prototype.backWardString = function(){ //向后遍历输出
let current = this.head
let result = " "
while(current){
result += current.data + " "
current = current.next
}
return result
}
DoubleLinkedList.prototype.forWardString = function(){ //像前遍历输出
current = this.tail
let result = " "
while(current){
result += current.data + " "
current = current.pre
}
return result
}
DoubleLinkedList.prototype.insert = function(position , data){ //插入
if(position < 0 || position > this.length)
return false
let newnode = new Node(data)
if(this.length == 0){
this.head = newnode
this.tail = newnode
}else if(position == this.length){
newnode.pre = this.tail
this.tail.next = newnode
this.tail = newnode
}else{ //插入节点的思路:
let index = 0 //首先区分链表的状态和插入的位置,因为插入操作稍有不同
let current = this.head //插入的时候 一般先确定新插入节点的指向问题 这样就不会影响初始链表结构
while(index++ < position){ //然后再将插入位置的前后节点指向这个新节点
current = current.next
}
newnode.next = current
newnode.pre = current.pre
current.pre.next = newnode
current.pre = newnode
}
this.length +=1
}
DoubleLinkedList.prototype.indexOf = function(data){ //检索
let index = 0
let current = this.head
while(current){
if(current.data == data){
return index
}
current = current.next
index +=1
}
}
}
//测试
let test = new DoubleLinkedList()
test.append("1")
test.append("2")
test.append("3")
test.append("4")
test.append("5")
alert(test) //12345
alert(test.forWardString())//54321
alert(test.backWardString())//12345
test.insert(3,"a")
3.集合(es5)
集合中的元素,没有顺序,而且不能重复
function Set(){
//集合属性
this.items = {}
//方法
//添加add
Set.prototype.add = function(value){
//判断当前集合是否已经包含该元素
if(this.has(value)){
return false
}
//将元素添加到集合中
this.items[value] = value
return true
}
//has方法
Set.prototype.has = function(value){
return this.items.hasOwnProperty(value)
}
//remove方法
Set.prototype.remove = function (value){
//判断该集合是否包含该元素
if(!this.has(value)){
return false
}
delete this.items[value]
return true
}
//clear 方法
Set.prototype.clear = function(){
this.items = {}
}
//size方法
Set.prototype.size = function(){
return Object.keys(this.items).length
}
//获取集合中所有的值
Set.prototype.values = function(){
return Object.keys(this.items)
}
}
4.栈(以下为es6,借鉴他人总结)
栈和队列本质上也是数组,是较为特殊的两种。
栈是一种后入先出的有序集合,例如 js 的执行栈。
新添加的元素放入栈顶
push,要删除元素也必须只能从栈顶删除pop,就如同只有一个端口的筒
代码的实现
let Stack = (function(){
let items = new WeakMap()
class Stack {
constructor () {
items.set(this, [])
}
pop () { // 出栈
return items.get(this).pop()
}
push (v) { // 入栈
items.get(this).push(v)
}
peek () { // 获取当前栈顶
return items.get(this)[items.get(this).length - 1]
}
size () { // 栈长度
return items.get(this).length
}
isEmpty () { // 栈是否为空
return items.get(this).length === 0
}
clear () { // 清空栈
items.get(this).length = 0
}
}
return Stack
})()
5.队列
队列是一种先进先出的有序集合
新添加的元素放在队尾
pop,要删除元素也必须只能从队首删除shift
let Queue = (function() {
let items = new WeakMap()
class Queue {
constructor () {
items.set(this, [])
}
enqueue (v) { // 入列
items.get(this).push(v)
}
dequeue () { // 出列
return items.get(this).shift()
}
front () { // 获取当前队列首位
return items.get(this)[0]
}
size () { // 栈长度
return items.get(this).length
}
isEmpty () { // 栈是否为空
return items.get(this).length === 0
}
clear () { // 清空栈
items.get(this).length = 0
}
}
return Queue
})()
6.二叉搜索树
二叉搜索树是二叉树的一种,它只允许你在左侧储存比父节点小的值,在右侧储存比父节点大的值。这样的定义对于向树的节点中查找/插入/删除节点非常高效
实现
const BinarySearchTree = (function(){
const Node = function (key) {
this.key = key
this.left = null
this.right = null
}
const insertNode = function (node, newNode) { // 插入节点辅助函数
if (newNode.key < node.key) {
if (node.left) {
insertNode(node.left, newNode)
} else {
node.left = newNode
}
} else {
if (node.right) {
insertNode(node.right, newNode)
} else {
node.right = newNode
}
}
}
const searchNode = function (node, key) { // 搜索节点辅助函数
if (!node) {
return false
}
if (key < node.key) {
return searchNode(node.left, key)
} else if (key > node.key) {
return searchNode(node.right, key)
} else {
return true
}
}
const minNode = function (node) { // 找到最小节点并返回key
if (!node) {
return null
}
if (node.left) {
return minNode(node.left)
} else {
return node.key
}
}
const maxNode = function (node) { // 找到最大节点并返回key
if (!node) {
return null
}
if (node.right) {
return maxNode(node.right)
} else {
return node.key
}
}
const findMinNode = function (node) { // 找到最小节点并返回node对象
if (!node) {
return null
}
if (node.left) {
return findMinNode(node.left)
} else {
return node
}
}
const removeNode = function (node, key) { // 移除节点并返回传入的 node
if (node === null) {
return null
}
if (key < node.key) { // 这种情况需要更新node.left,然后返回更新了node.left的新的node
node.left = removeNode(node.left, key)
return node
} else if (key > node.key) { // 这种情况需要更新node.right,然后返回更新了node.right的新的node
node.right = removeNode(node.right, key)
return node
} else { // 这种情况需要更新node.key或者其他更新手段(包括直接将node变为null, 或更新node.right),返回的也是更新后的node
// 情况1,被移除的是叶子节点
if (node.left === null && node.right === null) {
node = null
return node
}
// 情况2,被移除的是只有一个子节点的节点
if (node.left === null) { // 只有右子节点
node = node.right
return node
} else if (node.right === null) {//只有左子节点
node = node.left
return node
}
// 情况3,被移除的是有两个子节点的节点
const aux = findMinNode(node.right) // 找到子树中的最小节点,它肯定是一个叶子节点
node.key = aux.key // 将node的key设置为aux的key,达到删除效果,但此时有两个一样的key
node.right = removeNode(node.right, aux.key) // 移除以node.right为root的树上的重复的叶子节点aux.key
return node
}
}
class BinarySearchTree {
constructor () {
this.root = null
}
insert (key) { // 插入节点
let newNode = new Node(key)
if (!this.root) {
this.root = newNode
} else {
insertNode(this.root, newNode)
}
}
serach (key) { // 搜索节点,返回布尔值
return searchNode(this.root, key)
}
min () { // 最小节点
return minNode(this.root)
}
max () { // 最大节点
return maxNode(this.root)
}
remove (key) { // 删除节点
this.root = removeNode(this.root, key)
}
}
return BinarySearchTree
})()
7.红黑树规则
红黑树的基本性质
- 每个节点不是黑色就是红色
- 根节点是红色的
- 每个叶子节点是黑色的
- 如果一个节点是红色的,那么他的子节点一定会是黑色的
- 从任意一个节点到叶子节点,经过的黑色节点是一样多的
插入的五种情况
新节点为N,父节点为P,祖节点为G,叔节点为U,新节点默认为红色
- 第一种
- 新节点N位于树的根上
- 这种情况我们直接将红色变成黑色即可
- 第二种
- 新节点的父节点P是黑色
- 性质4没有失效(新节点是红色的),性质5也没有问题
- 尽管新节点有两个黑色的子节点nil,但是新节点N是红色的,所以通过它的路径中,黑色节点的个数依然相同,满足性质5
- 第三种
- P为红色,U也为红色
- 父红叔红祖黑 -》父黑叔黑祖红
操作方案
- 将P和U转变为黑色,并且G转换为红色
- 现在新节点N有了一个黑色的父节点P。所以每条路径的 黑色节点的数目没有变
- 而从更高的路劲上,必然都会经过G节点,所以那些路径的黑色节点数目也是不变的,符合性质5
可能出现的问题
- 但是,N的祖节点G的父节点也是红色的,这就违反了性质3,可以递归的调整颜色
- 但是如果递归调整颜色到了根节点,就需要进行旋转
- 第四种
- N的叔节点U是黑节点
- 父红叔黑祖黑 N是左儿子
- 父黑
- 祖红
- 右旋转
操作方案
- 对祖父节点依次进行右旋转
- 再旋转查收的树中,以前的父节点P现在是新节点,以前祖节点的父节点
- 交换以前的父节点,和组件点G的颜色
- B节点向右平移,称为G节点的左子节点
- 第五种
- N的树节点U是黑色节点,且N有自己的子节点
- 父红叔黑祖黑,N是右儿子
- 以P为根进行左旋转
- 将P作为新插入的红色节点即可
- 自己变成黑色
- 祖变成红色
- 以祖为根进行右旋转
操作方案
- 对p节点进行依次左旋转,形成情况四的结果
- 对祖父节点G进行一次右旋转,并且改变颜色即可