线性数据结构

254 阅读8分钟

线性数据结构(一维数据结构)

一维数据结构:数组链表

线性数据结构强调的是存储与顺序

线性数据结构之数组

数组定长

JS引擎会自动的为我们扩容,但是扩容的过程消耗 cpu,影响性能

并且数组扩容会导致,复制原先的数组,重新开辟一部分连续的空间,显然是很消耗性能的

数组a = [1, 2, 3, 4, 5];
[]表示存储地址的偏移
操作系统小知识:通过偏移查询数据性能最好

数组的特性:

  1. 数组的存储在物理空间上是连续的
  2. 底层的数组长度是不可变的
  3. 数组的变量指向了数组第一个元素的位置

优点:

  • 查询性能好(指定查询某个位置)

缺点:

  • 因为数组的存储空间必须是连续的,所以如果数组比较大,当系统的空间碎片较多的时候,容易存不下
  • 因为数组的长度是固定的,所以数组的内容难以被添加和删除

线性数据结构之链表

链表的特点:

  1. 在空间上不是连续的,可以是跳跃的
  2. 没存放一个值,都要多开销一个引用空间

优点:

  • 只要空间足够大,就能存的下,不用担心空间碎片的问题
  • 链表的添加和删除非常的容易

缺点:

  • 查询速度慢(查询某个位置,因为链表不像数组,他不是连续的)
  • 链表的每一个节点都需要创建一个指向 next 的引用,浪费一定的空间(当存放的数据越多的时候,这部分的开销的内存占据的空间就越少,因此适合存放大量数据)

每一个节点都认为自己是根节点

我想传递一个链表,我必须传递链表的根节点,因为每个节点没有记录谁指向了他,只记录了自己的值和自己指向的节点

var b = {
  value: 2,
  next: null,
};
var a = {
  value: 1,
  next: b,
};

console.log(a.next === b); //true

定义链表

// 创建一个链表
function Node(value) {
  this.value = value;
  this.next = null;
}
var a = new Node(1);
var b = new Node(2);
var c = new Node(3);
var d = new Node(4);

a.next = b;
b.next = c;
c.next = d;
d.next = null;

线性数据结构的遍历

遍历:将一个集合中的每一个元素进行获取并查看 算法必须要有严谨性判断

循环遍历链表

// 创建一个链表
function Node(value) {
  this.value = value;
  this.next = null;
}
var a = new Node(1);
var b = new Node(2);
var c = new Node(3);
var d = new Node(4);

a.next = b;
b.next = c;
c.next = d;
d.next = null;

function bianlink(root) {
  // 传递根节点
  var temp = root;
  while (true) {
    if (temp != null) {
      console.log(temp.value);
    } else {
      break;
    }
    // 每次循环重新赋值,最后一次循环temp=null
    temp = temp.next;
  }
}
bianlink(a); //1 2 3 4

递归遍历链表

// 递归遍历必须有出口
function Recursion(root) {
  if (root === null) return;
  console.log(root.value);
  Recursion(root.next);
}
Recursion(a);

链表的逆置

// 链表的逆置
function reLink(root) {
  if (root.next.next == null) {
    // 当前节点是倒数第二个节点
    root.next.next = root; //把最后一个节点指向倒数第二个节点
    return root.next; //返回新的头节点
  } else {
    // 其他节点,
    // 1.需要改变其他节点对自己的next指向
    // 2.改变自己的next指向
    var res = reLink(root.next);
    root.next.next = root;
    root.next = null;
    return res;
  }
}

var newLink = reLink(a);

Recursion(newLink);

leetcode.234-回文链表

Example:
   input:head = [1221]
   output: true
Example:
   input:head = [1,2]
   output: false

题解:遍历链表把每个节点的值放到一个栈中保存,然后我们再次遍历这个链表,每次遍历的同时出栈一个元素,出栈的元素和当前遍历的节点值相比较,不等返回false,反之为true。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {boolean}
 */
var isPalindrome = function (head) {
  let stackValue = []; //一个栈
  getNodeVal(head); //节点全部入栈

  // 然后就是出栈然后比较pop返回出栈的数值
  let res = compare(head, stackValue);

  function compare(head, stack) {
    if (head == null || stack.length == 0) return;
    while (head) {
      let val = head.val;
      if (val != stack.pop()) {
        return false;
      } else {
        head = head.next;
      }
    }
    return true;
  }

  function getNodeVal(root) {
    if (root == null) {
      return;
    }
    stackValue.push(root.val);
    getNodeVal(root.next);
    return stackValue;
  }
     return res;
};

有环的链表

如下图所示:图中的链表就形成了一个闭环,这个环的交点就是节点3

下面提供了两种方法

  • 方法一:Set 是否重复 根据Set的特性,我们遍历这个链表,每次遍历节点就把这个节点放到set中,然后继续遍历节点,如果这个链表没有环,那么set中就不会有重复,否则就直接返回这个重复的节点
  • 方法二:快慢指针 这个方法,怎么说你知道这个方法那就这样写就对了,你不知道就白瞎了(因为这个你要问我怎么证明我也不知道,但是就是这么写的);
    • 定义两个指针,快指针p1慢指针p2,
    • 快指针一次走两步,慢指针一次走一步
    • 因为如果是一个有环的链表,那么快慢指针会在这个环上循环,并且一定会相遇,当二者相遇时,快指针回到头节点的位置,慢指针还在那个位置;(如果没有环,那么一定是快指针先遇到 null,直接返回 null 就 ok 了)
    • 随后快慢指针一起一次一步开始移动,当二者再次相遇时,相遇的节点就是所求的交点
function ListNode(val, next) {
  this.val = val === undefined ? 0 : val;
  this.next = next === undefined ? null : next;
}

var a = new ListNode(1);
var b = new ListNode(2);
var c = new ListNode(3);
var d = new ListNode(4);

a.next = b;
b.next = c;
c.next = d;

// 用set判断一个链表是否有环
// 判断一个链表是不是闭环
function isCircle(head) {
  // 一个单链表有环,那么我们遍历整个链表一定说死循环的,
  // 不存在node.next == null的情况;
  // 我们循环到一个节点,把他放入set,如果循环中遇到set有这个节点就说明有环
  // 能一直遍历到null就说明无环
  let set = new Set();
  let bool = blink(head);
  function blink(root) {
    if (root == null) return;
    if (set.has(root)) {
      return root;
    }
    set.add(root);
    return blink(root.next);
  }

  return bool ? true : false;
}

console.log("链表是否有环:", isCircle(a));

console.log("--------------------------------");

// 快慢指针法
// 判断一个链表是否有环,有环的话返回相交的节点,没有的话返回null
function getPublicNode(head) {
  if (head == null || head.next == null || head.next.next == null) return null;
  let p1 = head; //快指针
  let p2 = head; //慢指针
  // 先让他们各走一步,错开
  p1 = p1.next.next;
  p2 = p2.next;
  while (p1 != p2) {
    // 没有相遇继续走
    if (p1.next == null || p1.next.next == null) {
      return null; //快指针肯定先走完整个链表,如果有环不可能有null
    }
    p1 = p1.next.next;
    p2 = p2.next;
  }
  // 跳出循环,已经相遇
  p1 = head; //快指针回到头部
  // 随后快慢指针各走一步,二者交的点就是环的交点
  while (p1 != p2) {
    p1 = p1.next;
    p2 = p2.next;
  }
  // 跳出循环,二者又相遇,相遇即所求
  return p2;
}
console.log("相交节点:", getPublicNode(a));

两个链表判断是否有交点,如果有返回一个交点,没有则返回 null(多种条件分析,再上面的方法基础上讨论几种特殊场景,建议先弄懂上面的例题再看,刚开始看不太好理解)

一个有环一个无环

两个有环

function ListNode(val, next) {
  this.val = val === undefined ? 0 : val;
  this.next = next === undefined ? null : next;
}

// 蜗牛形
// var a1 = new ListNode(1);
// var a2 = new ListNode(1);

// var b1 = new ListNode(2);
// var b2 = new ListNode(2);
// var b3 = new ListNode(2);

// var c = new ListNode(3);
// var d = new ListNode(4);
// var e = new ListNode(5);

// // 链表1
// a1.next = a2;
// a2.next = c;
// c.next = d;
// d.next = e;
// e.next = c;

// // 链表2
// b1.next = b2;
// b2.next = b3;
// b3.next = c;

// 环形
// let head1 = new ListNode(1);
// let head2 = new ListNode(2);
// let head3 = new ListNode(3);
// let head4 = new ListNode(4);

// head1.next = head2;
// head2.next = head3;
// head3.next = head4;
// head4.next = head2;

// let headA = new ListNode("a");
// let headB = new ListNode("b");
// let headC = new ListNode("c");
// let headD = new ListNode("d");

// headA.next = headB;
// headB.next = headC;
// headC.next = head4;

// 两个无环相交链表形
// let no1 = new ListNode(1);
// let no2 = new ListNode(2);
// let no3 = new ListNode(3);
// let no4 = new ListNode(4);
// no1.next = no2;
// no2.next = no3;
// no3.next = no4;

// let y1 = new ListNode("a");
// let y2 = new ListNode("b");

// y1.next = y2;
// y2.next = no3;

// 两个链表判断是否相交
/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function (headA, headB) {
  var p1 = getNode(headA); //链表A的交点
  var p2 = getNode(headB); //链表B的交点
  if (p1 == null && p2 == null) {
    //两个链表各自没有交点,可能两个链表交叉
    let n = headA;
    let m = headB;
    let lengthDis = 0; //记录两个链表差值
    while (n.next != null && n.next.next != null) {
      lengthDis++;
      n = n.next;
    }
    // 遍历另一个链表
    while (m.next != null && m.next.next != null) {
      lengthDis--;
      m = m.next;
    }
    lengthDis = Math.abs(lengthDis);
    n = lengthDis > 0 ? headA : headB; //让n指向长的链表
    m = n == headA ? headB : headA;
    while (lengthDis > 0) {
      n = n.next;
    }
    // 这个时候两个链表同时移动的相交点就是所求
    while (n.next != null) {
      if (n == m) {
        return n;
      }
      n = n.next;
      m = m.next;
    }
    return null;
  } else if ((p1 == null && p2 != null) || (p2 == null && p1 != null))
    return null;
  else {
    // 两个有环的链表又分为三种情况
    // 情况一:两个不相交的环
    if (p1 == p2) {
      return p1;
    } else {
      // 两个交点不相等
      // 任意让其中一个点再走一遍,另一个点不动,如果能遇到就说明两个相交,否则说明不相交
      let end = p1;
      p1 = p1.next;
      while (p1 != end) {
        if (p1 == p2) {
          return p1;
        }
        p1 = p1.next;
      }
      return null;
    }
  }

  // 返回一条链表的交点,没有交点就返回null
  function getNode(head) {
    // 为什么要这样判断,对于一条链表而言节点小于3就无法有交点
    if (head == null || head.next == null || head.next.next == null)
      return null;
    let p1 = head; //快指针
    let p2 = head; //慢指针
    p1 = p1.next.next; //为了避免从同一起点开始,先让他们各动一次
    p2 = p2.next;
    while (p1 != p2) {
      // 如果这个链表无环,那么一定是快指针先到null
      if (p1.next == null || p1.next.next == null) {
        return null;
      }
      // 继续移动
      p1 = p1.next.next;
      p2 = p2.next;
    }
    // 此时说明p1、p2在环上相遇
    p1 = head; //快指针回到头部
    while (p1 != p2) {
      p1 = p1.next;
      p2 = p2.next;
    }
    // 此时这个点就是所求
    return p1;
  }
};

console.log(getIntersectionNode(no1, y1));

双向链表

  • 优点

无论给出哪一个节点都能对整个链表实现遍历

  • 缺点

要多耗费一个引用空间,而且构建双向链表比较复杂

// 实现双链表
function Node(value) {
  this.value = value;
  this.next = null;
  this.last = null;
}

var node1 = new Node(1);
var node2 = new Node(2);
var node3 = new Node(3);
var node4 = new Node(4);

node1.next = node2;
node2.last = node1;
node2.next = node3;
node3.last = node2;
node3.next = node4;
node4.last = node3;