数据结构是数据的组织、管理和储存格式,其使用目的是为了高效的访问和修改数据.
其中可以分为物理结构和逻辑结构,
物理结构是在内存中实实在在的储存结构,有:数组、链表等
逻辑结构是抽象的存储结构,依赖于物理结构存在, 有:栈、队列、树、图等
数组
数组是最为简单、最为常见的数据结构.
数组在内存中顺序储存,因此也很好的实现了逻辑上的顺序表
内存是由一个个连续的内存单元组成的,每一个内存单元都有自己的地址,在这些内存单元中,有的被其他数据占用,有的是空闲的.
数组中的每一个元素,都储存在小小的内存单元中,并且元素之间紧密排列,既不能打乱元素的储存顺序,也不能跳过某个储存单元进行储存.
数组的操作
数组读取元素时间复杂度为 O(1)
更新元素时间复杂度为 O(1)
插入(头部插入、中间插入、尾部插入、超范围插入)的时间复杂度为 O(n),数组的扩容时间复杂度为O(n)
删除时间复杂度为O(n)
数组的优劣
数组拥有非常高效的随机访问能力, 只要给出下标,就可以用常量的时间找到对应的元素. 有一种高效查找元素的算法叫做二分查找,就是利用数组的这个优势.
数组的劣势体现在插入和删除元素方面,由于数组元素连续紧密的储存在内存中,插入、删除元素都会导致大量元素被迫移动,影响效率.
总体上数组适合读操作多,写操作少的场景
链表
链表是一种在物理上非连续、非顺序的数据结构,由若干节点(node)组成
单向链表
单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针 next
interface Node{
data:any,
next:Node
}
链表中的第一个节点被称为头节点,最后一个节点被称为尾节点,尾节点的next指针指向空
与数组按照下标来随机寻找元素不同,对于链表的其中一个节点,只能根据节点A的next指针来找到下一个节点B,再根据节点B的next指针找到下一个节点C.......
双向链表
双向链表具有回溯到它前置节点的能力
双向链表的每一个节点除了拥有 data 和 next 指针, 还拥有指向前置节点的 prev 指针
链表的储存方式
如果说数组在内存中的储存方式是顺序储存, 那么链表在内存中的储存方式则是随机储存
什么叫随机储存, 数组在内存中占用了连续完整的内存空间,而链表则是采用了见缝插针的方式,这样可以灵活有效的利用零散的碎片空间.
图中的箭头代表链表节点的next指针
链表的基本操作
1. 查找
在查找元素时, 链表不像数组那样可以根据下标快速定位,只能从头节点开始,向后一个一个节点逐一查找, 所以时间复杂度为O(n)
2. 更新
尾部插入、头部插入、中间插入的时间复杂度都为O(1)
3. 删除
尾部删除、头部删除、中间删除的时间复杂度都为O(1)
实现代码
interface LinkedNodeType{
data: any;
next: LinkedNodeType | null;
}
class LinkedNode{
data: any
next: LinkedNodeType | null
public constructor(data: any){
this.data = data;
this.next = null;
}
}
class LinkedData{
size: number;
head: LinkedNodeType|null;
last: LinkedNodeType|null;
constructor(){
this.size = 0; // 链表实际长度
this.head = null; // 头节点指针
this.last = null; // 尾节点指针
}
/**
* @description 插入元素操作
* @param {*} data 插入的元素
* @param {*} index 插入的位置
*/
insert(data:any, index:number): void{
if(index < 0 || index > this.size) {
throw new Error('超出链表节点范围');
}
const node = new LinkedNode(data);
if(this.size === 0) {
// 插入头部
this.head = node;
this.last = node;
} else if(this.size === index){
// 插入尾部
(this.last as LinkedNode).next = node;
this.last = node;
} else {
// 插入中间
const prevNode = this.getNode(index - 1);
const nextNode = this.getNode(index + 1);
prevNode.next = node;
nextNode.next = nextNode;
}
this.size++;
}
/**
*
* @description 查找元素操作
* @param {number} index
* @memberof LinkedData
* @returns 返回查找的节点
*/
getNode(index:number): LinkedNodeType {
if(index < 0 || index >= this.size) {
throw new Error('超出链表节点范围');
}
let temp = (this.head as LinkedNode);
for (let i = 0; i < index; i++) {
temp = (temp.next as LinkedNode);
}
return temp;
}
/**
* @description 删除节点
* @param {*} index
* @memberof LinkedData
*/
remove(index:number): LinkedNodeType {
if(index < 0 || index >= this.size) {
throw new Error('超出链表节点范围');
}
let removeNode = null;
if(index === 0) {
// 删除头部节点
removeNode = this.head;
this.head = (this.head as LinkedNode).next;
} else if(index === this.size - 1) {
// 删除尾部节点
const preNode = this.getNode(index - 1);
removeNode = preNode.next;
preNode.next = null;
this.last = preNode;
} else {
// 删除中间节点
const prevNode = this.getNode(index - 1);
const nextNode = (prevNode.next as LinkedNode).next;
removeNode = prevNode.next;
prevNode.next = nextNode;
}
this.size--;
return removeNode as LinkedNode;
}
/**
* @description 输出链表
* @memberof LinkedData
*/
outPut(): void{
let temp = this.head;
while(temp !== null){
console.log(temp.data)
temp = temp.next;
}
}
}
const link = new LinkedData();
link.insert('hello', 0);
link.insert('hi', 1);
link.insert('第三个', 2);
link.insert('第四个', 3);
link.remove(0);
link.outPut();
数组VS链表
数组和链表各有千秋:
数组更适合读多 写少的操作,链表更适合读少,写多的操作
摘要总结自: 漫画算法 小灰的算法之旅