提起算法不少CS毕业的人都不会陌生,可是无论你是在学校理论知识学的如何扎实仍是在学校中有参加比赛的经历(ACM等),可是到了工做中由于没有实际的应用场景或者说应用场景不多,致使一些本来顺手拈来的知识点和操做都感到很生疏。javascript
同时,因为本人如今专职于前端工做(原来是先后端都作),不少的应用场景都没有涉及算法的身影。这也是前端在技术鄙视链的顶端。其实前端也是有不少顶级的算法思路和实现。只是别人帮你实现了,你直接就用。前端
因此我始终相信一个信条:java
实践出真知,温故而知新node
因此,接下来的文档,将一块儿汇总一下我跟着极客时间的数据结构与算法之美的课程来一块儿回顾和总结一下(内容大部分是极客时间的,可是有一些也是我查询相关资料来进行比对和校验的)。c++
该课程只是我的的学习文档和看法,若有不对和理解有误差的地方,请不吝赐教。算法
渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增加关系,能够粗略地表示,越高阶复杂度的算法,执行效率越低后端
从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2 )数组
说到数组你们想必在开发中,是接触最多的一类数量类型,也是最多见的一类线性表。浏览器
线性表的分类 bash
![]()
一种线性表数据结构。它用一组连续的内存空间,来存储一组具备相同类型的数据。
As JavaScript arrays are implemented as hash-maps(哈希表) or dictionaries(字典) and not contiguous.(非连续)
它并不须要一块连续的内存空间,它经过指针将一组零散的内存块串联起来使用
内存分配
链表经过指针将一组零散的内存块串联在一块儿。其中,咱们把内存块称为链表的结点
其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。咱们习惯性地把第一个结点叫做头结点,把最后一个结点叫做尾结点。其中,头结点用来记录链表的基地址。有了它,咱们就能够遍历获得整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。
循环链表的尾结点指针是指向链表的头结点
双向链表须要额外的两个空间来存储后继结点和前驱结点的地址。因此,若是存储一样多的数据,双向链表要比单链表占用更多的内存空间。
数组中随机访问,指的是按下标的随机访问
大体的结构以下:
size和head为LinkedList构造函数私有属性,size记录链表中有多少个节点,head指向链表的头结点
/**
* 自定义链表:对外公开的方法有
* append(element) 在链表最后追加节点
* insert(index, element) 根据索引index, 在索引位置插入节点
* remove(element) 删除节点
* removeAt(index) 删除指定索引节点
* removeAll(element) 删除全部匹配的节点
* set(index, element) 根据索引,修改对应索引的节点值
* get(index) 根据索引获取节点信息
* indexOf(element) 获取某个节点的索引位置
* clear() 清空全部节点
* length() 返回节点长度
* print() 打印全部节点信息
* toString() 打印全部节点信息,同print
* */
const LinkedList = function(){
let head = null;
let size = 0; //记录链表元素个数
//Node模型
function LinkNode(element, next){
this.element = element;
this.next = next;
}
//元素越界检查, 越界抛出异常
function outOfBounds(index){
if (index < 0 || index >= size){
throw("抱歉,目标位置不存在!");
}
}
//根据索引,获取目标对象
function node(index){
outOfBounds(index);
let obj = head;
for (let i = 0; i < index; i++){
obj = obj.next;
}
return obj;
}
//新增一个元素
function append(element){
if (size == 0){
head = new LinkNode(element, null);
}
else{
let obj = node(size-1);
obj.next = new LinkNode(element, null);
}
size++;
}
//插入一个元素
function insert(index, element){
if (index == 0){
head = new LinkNode(element, head);
}
else{
let obj = node(index-1);
obj.next = new LinkNode(element, obj.next);
}
size++;
}
//修改元素
function set(index, element){
let obj = node(index);
obj.element = element;
}
//根据值移除节点元素
function remove(element){
if (size < 1) return null;
if (head.element == element){
head = head.next;
size--;
return element;
}
else{
let temp = head;
while(temp.next){
if (temp.next.element == element){
temp.next = temp.next.next;
size--;
return element;
}
else{
temp = temp.next;
}
}
}
return null;
}
//根据索引移除节点
function removeAt(index){
outOfBounds(index);
let element = null;
if (index == 0){
element = head.element;
head = head.next;
}
else{
let prev = node(index-1);
element = prev.next.element;
prev.next = prev.next.next;
}
size--;
return element;
}
//移除链表里面的全部匹配值element的元素
function removeAll(element){
let virHead = new LinkNode(null, head); //建立一个虚拟头结点,head为次节点
let tempNode = virHead, ele = null;
while(tempNode.next){
if (tempNode.next.element == element){
tempNode.next = tempNode.next.next;
size--;
ele = element;
}
else{
tempNode = tempNode.next;
}
}
//从新赋值
head = virHead.next;
return ele;
}
//获取某个元素
function get(index){
return node(index).element;
}
//获取元素索引
function indexOf(element){
let obj = head, index = -1;
for (let i = 0; i < size; i++){
if (obj.element == element){
index = i;
break;
}
obj = obj.next;
}
return index;
}
//清除全部元素
function clear(){
head = null;
size = 0;
}
//属性转字符串
function getObjString(obj){
let str = "";
if (obj instanceof Array){
str += "[";
for (let i = 0; i < obj.length; i++){
str += getObjString(obj[i]);
}
str = str.substring(0, str.length - 2);
str += "], "
}
else if (obj instanceof Object){
str += "{";
for (var key in obj){
let item = obj[key];
str += "\"" + key + "\": " + getObjString(item);
}
str = str.substring(0, str.length-2);
str += "}, "
}
else if (typeof obj == "string"){
str += "\"" + obj + "\"" + ", ";
}
else{
str += obj + ", ";
}
return str;
}
function toString(){
let str = "", obj = head;
for (let i = 0; i < size; i++){
str += getObjString(obj.element);
obj = obj.next;
}
if (str.length > 0) str = str.substring(0, str.length -2);
return str;
}
//打印全部元素
function print(){
console.log(this.toString())
}
//对外公开方法
this.append = append;
this.insert = insert;
this.remove = remove;
this.removeAt = removeAt;
this.removeAll = removeAll;
this.set = set;
this.get = get;
this.indexOf = indexOf;
this.length = function(){
return size;
}
this.clear = clear;
this.print = print;
this.toString = toString;
}
////测试
// let obj = new LinkedList();
// let obj1 = { title: "全明星比赛", stores: [{name: "张飞vs岳飞", store: "2:3"}, { name: "关羽vs秦琼", store: "5:5"}]};
//
// obj.append(99);
// obj.append("hello")
// obj.append(true)
// obj.insert(3, obj1);
// obj.insert(0, [12, false, "Good", 81]);
// obj.print();
// console.log("obj1.index: ", obj.indexOf(obj1));
// obj.remove(0);
// obj.removeAll(obj1);
// obj.print();
////测试2
console.log("\n\n......test2.....")
var obj2 = new LinkedList();
obj2.append(8); obj2.insert(1,99); obj2.append('abc'); obj2.append(8); obj2.append(false);
obj2.append(12); obj2.append(8); obj2.append('123'); obj2.append(8);
obj2.print();
obj2.removeAll(8); //删除全部8
obj2.print();
复制代码
/**
*
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let prev = null;
let curr = head;//定义"哨兵"
while (curr != null) {
let nextTemp = curr.next;//暂存剩余链表
curr.next = prev;//与剩余链表断开链接,并将指向变动
prev = curr;//两个节点交互位置
curr = nextTemp;//指针下移
}
return prev;
};
复制代码
关于栈,有一个很是贴切的例子,就是一摞叠在一块儿的盘子。咱们平时放盘子的时候,都是从下往上一个一个放;取的时候,咱们也是从上往下一个一个地依次取,不能从中间任意抽出。后进者先出,先进者后出,这就是典型的“栈”结构。
从栈的操做特性上来看
栈是一种“操做受限”的线性表,只容许在一端插入和删除数据。
实际上,栈既能够用数组来实现,也能够用链表来实现。用数组实现的栈,咱们叫做顺序栈,用链表实现的栈,咱们叫做链式栈。(这也仅仅针对的是C,C++,JAVA等静态类型语言)
操做系统给每一个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量做为一个栈帧入栈,当被调用函数执行完成,返回以后,将这个函数对应的栈帧出栈。
function main() {
let a = 1;
let ret = 0;
let res = 0;
ret = add(3, 5);
res = a + ret;
console.log(`处理结果为 ${res}`);
reuturn 0;
}
function add( x, y) {
let sum = 0;
sum = x + y;
return sum;
}
复制代码
图中显示的是,在执行到 add() 函数时,函数调用栈的状况。
实际上,编译器就是经过两个栈来实现的。其中一个保存操做数的栈,另外一个是保存运算符的栈。咱们从左向右遍历表达式,当遇到数字,咱们就直接压入操做数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
将 3+5*8-6 这个表达式的计算过程画成了一张图,以下:
使用两个栈,X 和 Y,咱们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当咱们点击前进按钮时,咱们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面能够继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面能够点击前进按钮浏览了。
如此反复如上操做,就能够简单解释浏览器回退和前进的操做步骤。
内存中的堆栈和数据结构堆栈不是一个概念,能够说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
队列跟栈同样,也是一种操做受限的线性表数据结构。
队列跟栈同样,也是一种抽象的数据结构。它具有先进先出的特性,支持在队尾插入元素,在队头删除元素。
队列能够用数组来实现,也能够用链表来实现。用数组实现的栈叫做顺序栈,用链表实现的栈叫做链式栈。一样,用数组实现的队列叫做顺序队列,用链表实现的队列叫做链式队列。
// 用数组实现的队列
const ArrayQueue=function(){
// 数组:items,数组大小:n
let items =[];
let n = 0;
// head 表示队头下标,tail 表示队尾下标
let head = 0;
let tail = 0;
// 申请一个大小为 capacity 的数组
function createArrayQueue(capacity) {
items = new Array(capacity);
n = capacity;
}
// 入队操做,将 item 放入队尾
function enqueue(item) {
// tail == n 表示队列末尾没有空间了
if (tail == n) {
// tail ==n && head==0,表示整个队列都占满了
if (head == 0) return false;
// 数据搬移
for (int i = head; i < tail; ++i) {
items[i-head] = items[i];
}
// 搬移完以后从新更新 head 和 tail
tail -= head;
head = 0;
}
items[tail] = item;
++tail;
return true;
}
// 出队
function dequeue() {
// 若是 head == tail 表示队列为空
if (head == tail) return null;
// 为了让其余语言的同窗看的更加明确,把 -- 操做放到单独一行来写了
let ret = items[head];
++head;
return ret;
}
this.createArrayQueue = createArrayQueue;
this.enqueue = enqueue;
this.dequeue = dequeue;
}
复制代码
用数组实现的队列,在 tail==n 时,会有数据搬移操做,这样入队操做性能就会受到影响。
想要避免数据搬移,能够用循环队列来处理。
咱们能够看到,图中这个队列的大小为 8,当前 head=4,tail=7。当有一个新的元素 a 入队时,咱们放入下标为 7 的位置。但这个时候,咱们并不把 tail 更新为 8,而是将其在环中后移一位,到下标为 0 的位置。当再有一个元素 b 入队时,咱们将 b 放入下标为 0 的位置,而后 tail 加 1 更新为 1。因此,在 a,b 依次入队以后,循环队列中的元素就变成了下面的样子:
经过这样的方法,咱们成功避免了数据搬移操做。
const CircularQueue = function {
// 数组:items,数组大小:n
let items;
let n = 0;
// head 表示队头下标,tail 表示队尾下标
let head = 0;
let tail = 0;
// 申请一个大小为 capacity 的数组
function createCircularQueuee(capacity) {
items = new Array(capacity);
n = capacity;
}
// 入队
function enqueue(item) {
// 队列满了
if ((tail + 1) % n == head) return false;
items[tail] = item;
tail = (tail + 1) % n;
return true;
}
// 出队
function dequeue() {
// 若是 head == tail 表示队列为空
if (head == tail) return null;
String ret = items[head];
head = (head + 1) % n;
return ret;
}
this.createCircularQueuee = createCircularQueuee;
this.enqueue = enqueue;
this.dequeue = dequeue;
}
复制代码
未完待续 续集: