数据结构与算法入门学习(自用)

221 阅读9分钟

数据结构

数据结构指的是数据之间的相互关系,它一般包括以下三个方面的内容:

  • 数据的逻辑结构:数据之间的逻辑关系
  • 数据的存储结构:数据元素及其关系在计算机存储器内的表示
  • 数据的运算,即对数据进行的操作

逻辑结构

根据数据逻辑关系的不同,可分为四种基本结构类型

  • 集合 :数据具有符合某一条件的相同的性质,且别无其他关系。
  • 线性结构:数据之间存在一对一的关系。
  • 树形结构:数据之间存在一对多的关系。
  • 图形结构:数据之间存在多对多的关系。

存储结构

存储方法分类:

  • 顺序存储方法: 用一组地址连续的存储单元依次存储线性表的各个数据元素
  • 链式存储方法:用一组任意的存储单元存储线性表中的各个数据元素

概览

微信截图_20220228223239.png

线性结构

线性表

零个或多个数据元素的有限序列。

顺序存储结构

把线性表的各个数据元素依次存储在一组地址连续的存储单元里 ;用这种方法存储的线性表简称为顺序表 image.png 链式存储结构

用一组任意的存储单元来存储线性表的数据元素

数据域(Data Field):存储数据元素信息的域

指针域(Link Field):存放直接后继结点或前驱结点地址的域

image.png

栈是一种遵从先进后出 (LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。

image.png

队列

队列是一种先进先出(FIRST IN FIRST OUT 简称FIFO)的线性表;只允许在表的一端进行插入,在另一端删除元素;允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)

image.png 优先队列

优先队列的思想是将最重要的元素赋予最小的优先级(从代价角度)或最大的优先级(从利润角度)

链表

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个 元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。 链表分类.png

双向链表.png

循环链表.png

非线性结构

包含n(n>0)个节点的有穷集合K,且在K中定义了一个关系N,N满足以下条件:

  • 有且仅有一个结点K0,它对于关系N来说没有前驱,称K0为树的根结点
  • 除K0外,K中的每个结点对于关系N来说有且仅有一个前驱
  • K中各结点对关系N来说可以有m个后继(m≥0)

树术语.png

二叉树

二叉树定义.png

满二叉树.png

完全二叉树.png

堆(特殊二叉树)

堆的特性:

  • 必须是完全二叉树
  • 用数组实现
  • 任一结点的值是其子树所有结点的最大值或最小值
    • 最大值时,称为“最大堆”,也称大顶堆;
    • 最小值时,称为“最小堆”,也称小顶堆。

图是网络结构的抽象模型。图是一组由边连接的节点(或顶点),任何二元关系都可以用图来表示。 image.png

哈希表

哈希的基本原理是将给定的键值转换为偏移地址来检索记录。 键转换为地址是通过一种关系(公式)来完成的,这就是哈希(散列)函数。 image.png

算法

image.png

时间复杂度

把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。

没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。

常见的时间复杂度有:

  • O(1): Constant Complexity: Constant 常数复杂度
  • O(log n): Logarithmic Complexity: 对数复杂度
  • O(n): Linear Complexity: 线性时间复杂度
  • O(n^2): N square Complexity 平⽅方
  • O(n^3): N square Complexity ⽴立⽅方
  • O(2^n): Exponential Growth 指数
  • O(n!): Factorial 阶乘

时间复杂度.png

时间复杂度效率.png

空间复杂度

算法所需存储空间的度量 image.png

基础算法

排序与查找算法

十大经典排序算法(动图演示)

自己刚入门前端练习的算法

冒泡排序

  //--------------------------
  for(var i=0,len=arr.length;i<len;i++){
      for(var j=i+1;j<len;j++){
        if(arr[i]>arr[j]){  //将最大的放在最后
            var temp=0;
            t=arr[i]; 
            arr[i]=arr[j];
            arr[j]=temp;
        }
      }
  }
  //--------第二种(改善)------------
  for(var i=1,len=arr.length;i<len;i++){
      for(var j=0;j<len-i;j++){  //已经排序好的最大数组不必在比较
        if(arr[j]>arr[j+1]){  
            var temp=0;
            t=arr[i]; 
            arr[i]=arr[j];
            arr[j]=temp;
        }
      }
  }

选择排序

  for(var i=0,len=arr.length-1;i<len;i++){
      int min = i;
      for(var j=i+1;j<len;j++){
        if(arr[min]>arr[j]){
            min=j;
        }
    }    
    if(min!=i){
        var temp=0;
        t=arr[min];
        arr[min]=arr[i];
        arr[i]=temp;
    }    
  }

直接插入排序

  for (int i = 1; i < arr.Length; i++){
    if (arr[i - 1] > arr[i])
    {
        int temp = arr[i];  //每次取出当前的要比较的值
        int j = i;
        while (j > 0 && arr[j - 1] > temp) //大于它的话,要赋值给它
        {
            arr[j] = arr[j - 1];
            j--;
        }
        arr[j] = temp;
    }
}

快速排序

//-----------------first---------------
var quickSort = function(arr){
    var arr = arr.concat(); //concat的新用法(深复制)
    if(arr.length<=1) return arr;
    var index = Math.floor(arr.length/2);
    var centerValue = arr.splice(index,1);
    //console.log(centerValue);
    var left = [];
    var right = [];
    for(var i=0,len=arr.length;i<len;i++){
        if(centerValue>=arr[i]){
            left.push(arr[i]);
        }else{
            right.push(arr[i]);
        }
    }
    //console.log(quickSort(left));
    //console.log(right);
    var res1 = arguments.callee(left);
    var res2 = arguments.callee(right);
    //return left.concat(right);
    return res1.concat(centerValue,res2);
}
var arr=[9,8,7,4,5,3,77];          
  var result = quickSort(arr);
  console.log(result);
  console.log(arr);
//-----------------second---------------
  var quickSort = function(arr){
    if(arr.length<=1) return arr;
    var index = Math.floor(arr.length/2);
    console.log(index); //1
    var centerValue = arr.slice(index,index+1)[0];
    console.log(centerValue); //5
    var left = [];
    var right = [];
    for(var i=0,len=arr.length;i<len;i++){
        if(centerValue>=arr[i]){
            left.push(arr[i]);
        }else{
            right.push(arr[i]);
        }
    }
    var res1 = arguments.callee(left);
    var res2 = arguments.callee(right);
    return res1.concat(res2);
}
var arr=[9,8,7,4,5,3,4,77];          
 //var result = quickSort(arr);
 //console.log(result);
 console.log(arr);
 var aa=[3,4];
 //console.log(aa.slice(1,2));
 console.log(aa.splice(0,0));

反转排序

 for(var i=0,len=arr.length-1;i<len/2;i++){
    var temp=0;
    t=arr[i];
    arr[i]=arr[len-1-i];
    arr[len-1-i]=temp;    
 }

参数排序

function mySort() {
    var tags = new Array();//使用数组作为参数存储容器
    tags = Array.prototype.slice.call(arguments);
    tags.sort(function(a,b){
        return a-b;
    });
    return tags;//返回已经排序的数组
}
var result = mySort(50,11,16,32,24,99,57,100); //传入参数个数不确定
console.info(result); //显示结果

二分查找

--迭代--
var searchInsert = function(nums, target) {
    let l = 0
    let r = nums.length - 1
    while(l <= r) {
        let mid = l + ((r - l) >> 1)

        if (nums[mid] === target) {
            return mid
        } else if (nums[mid] < target) {
            l = mid + 1
        } else if (nums[mid] > target) {
            r = mid - 1
        }
    }
    return -1
};
--递归--
var searchInsert = function(nums, target) {
    function binarySearch(l, r) {
        if (l <= r) {
            const mid = l + ((r - l) >> 1)
            if (nums[mid] === target) {
                return mid
            } else if (nums[mid] < target) {
                return binarySearch(mid + 1, r)
            } else if (nums[mid] > target) {
                return binarySearch(l, mid - 1)
            }
        }
        return -1
    }
    return binarySearch(0, nums.length - 1)
};

动态规划

告别动态规划,连刷40道动规算法题,我总结了动规的套路

Redraiment是走梅花桩的高手。Redraiment可以选择任意一个起点,从前到后,但只能从低处往高处的桩子走。他希望走的步数最多,你能替Redraiment研究他最多走的步数吗?

输入:
6
2 5 1 5 4 5 
输出:
3
说明:
6个点的高度各为 2 5 1 5 4 5
如从第1格开始走,最多为3步, 2 4 5 ,下标分别是 1 5 6
从第2格开始走,最多只有1步,5
而从第3格开始走最多有3步,1 4 5, 下标分别是 3 5 6
从第5格开始走最多有2步,4 5, 下标分别是 5 6
所以这个结果是3。 
function maxStep(arr) {
    let max = 0
    const temp = new Array(arr.length)
    for(let i = 0; i<arr.length; i++) {
        temp[i] = 1
        for(let j =0;j<i;j++) {
            if(arr[j] < arr[i]) {
                temp[i] = Math.max(temp[j] + 1, temp[i])
            }
        }
        max = Math.max(temp[i] , max)
    }
    return max
}

贪心算法

是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法

算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果

55. 跳跃游戏 主要思路为:如果一个位置能够到达,那么这个位置左侧所有位置都能到达

var canJump = function(nums) {
    const len = nums.length
    let max = 0
    for(let i =0;i<len;i++) { 
        if (i > max) {
            return false
        }
        max = Math.max(i+nums[i], max)
        if (max >= len -1) return true
    }
    return false
}

45. 跳跃游戏 II 主要思路:跳跃最小次数,反向就是每次找到可到达的最远位置

var jump = function(nums) {
    if (nums.length <= 1) {
        return 0
    }
    let max = 0
    let step = 0
    let end = 0
    for(let i = 0;i<nums.length;i++) {
        max = Math.max(max, i+nums[i])
        if (max >= nums.length - 1) {
            return step+1
        }
        if (i === end) {
            step++
            end = max
        }
    }
    return step
}

动态归划

var jump = function (nums) {
  var len = nums.length
  var dp = []
  dp[0] = 0
  for (var i = 1; i < len; i++) {
    dp[i] = Number.MAX_VALUE
    for (var j = 0; j < i; j++) {
      if (j + nums[j] >= i) {
        dp[i] = Math.min(dp[i], dp[j] + 1)
      }
    }
  }
  return dp[len - 1]
}

二叉树

先序遍历(根左右)
后序遍历(左右根)

中序遍历(左根右)

递归

(root) => {
  const res = [];
  const inorder = (root) => {
    if (root == null) {
      return;
    }
    inorder(root.left);
    res.push(root.val);
    inorder(root.right);
  };
  inorder(root);
  return res;
};

迭代

(root) => {
  const res = [];
  const stack = [];

  while (root) {        // 能压栈的左子节点都压进来
    stack.push(root);
    root = root.left;
  }
  while (stack.length) {
    let node = stack.pop(); // 栈顶的节点出栈
    res.push(node.val);     // 在压入右子树之前,处理它的数值部分(因为中序遍历)
    node = node.right;      // 获取它的右子树
    while (node) {          // 右子树存在,执行while循环    
      stack.push(node);     // 压入当前root
      node = node.left;     // 不断压入左子节点
    }
  }
  return res;
};

层序遍历

function(root) {
 const res = []
 if (!root) {
    return res;
 }
 const q = []
 q.push(root)
 while(q.length) {
    res.push([])
    const len = q.length
    for(let i =0;i<len;i++) {
        const node = q.shift()
        res[res.length - 1].push(node.val)
        if (node.left) q.push(node.left)
        if (node.right) q.push(node.right)
    }
 }
 return res
}

DFS

DFS可谓是“不撞南墙,不回头”。就像走迷宫一样,选择一条路一直走下去,当撞南墙了,才返回分叉口去试下一条路。

具体算法描述为:

选择一个起始点 [公式] 作为 当前结点,执行如下操作:
a. 访问 当前结点,并且标记该结点已被访问,然后跳转到 b;
b. 如果存在一个和 当前结点 相邻并且尚未被访问的结点 [公式] ,则将 [公式] 设为 当前结点,继续执行 a;
c. 如果不存在这样的 [公式] ,则进行回溯,回溯的过程就是回退 当前结点;\

上述所说的 当前结点 需要用一个栈来维护,每次访问到的结点入栈,回溯的时候出栈。除了栈,另一种实现深度优先搜索的方式是递归。

BFS

广度优先遍历呈现出「一层一层向外扩张」的特点,先看到的结点先遍历,后看到的结点后遍历,因此「广度优先遍历」可以借助「队列」实现。

  • 首先将节点加入队列Q。
  • 访问节点,同时将节点设置为已经被访问。
  • 循环从Q中取出节点:
    • 如果Q为空,则遍历结束。
    • 如果Q不为空,将该节点设置为已经访问。同时将其子节点加入Q进行排队。要观察子节点是否已经被访问或者已经加入了队列Q,否则将出现死循环,或者重复遍历

双指针

双指针技巧在处理数组和链表相关问题时经常用到,主要分为两类:左右指针快慢指针

所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。

对于单链表来说,大部分技巧都属于快慢指针。 5. 最长回文子串

中心扩散法(左右指针)

var longestPalindrome = function(s) {
    let result = ''
    for(let i = 0; i<s.length; i++) {
        const odd = palindromic(s, i, i)
        const even = palindromic(s, i, i+1)
        const maxStr = odd.length > even.length ? odd : even
        result = result.length > maxStr.length ? result : maxStr
    }
    return result
};

function palindromic(s, l, r) {
    while(l>=0 && r<s.length && s[l] === s[r]) {
        l--
        r++
    }
    return s.slice(l+1, r)
}

动态规划

function palindrome(s) {
  const n = s.length;
  let dp = Array.from(new Array(n), () => new Array(n).fill(false));
  let [left, right] = [0, 1];

  for (let i = 1; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (s[i] === s[j]) {
        if (i - j < 3) {
          dp[i][j] = true;
        } else {
          dp[i][j] = dp[i - 1][j + 1];
        }
      }
      if (dp[i][j] && i - j + 1 > right - left) [left, right] = [j, i + 1];
    }
  }
  return s.slice(left, right)
}

88. 合并两个有序数组

快慢双指针

function(nums1, m, nums2, n) {
    let p1 = 0, p2 = 0;
    const sorted = [];
    var cur;
    while (p1 < m || p2 < n) {
        if (p1 === m) {
           sorted.push(nums2[p2++])
        } else if (p2 === n) {
            sorted.push(nums1[p1++])
        } else if (nums1[p1] < nums2[p2]) {
            sorted.push(nums1[p1++])
        } else {
            sorted.push(nums2[p2++])
        }
    }
    for (let i = 0; i != m + n; ++i) {
        nums1[i] = sorted[i];
    }
};

滑动窗口

一种较难掌握的双指针技巧

主要思路是维持一个窗口,满足一定条件下,进行右滑扩大窗口,或者左滑缩小窗口 209. 长度最小的子数组

滑动窗口

var minSubArrayLen = function(target, nums) {
    let l = 0
    let r = 0
    const len = nums.length
    let sum = 0
    let min = Infinity
    while(r < len) {
        sum += nums[r]
        while(target <= sum) {
            min = Math.min(min, r-l+1)
            sum -= nums[l++]
        }
        r++
    }
    return min === Infinity ? 0: min
};

前缀和

核心思路是我们 new 一个新的数组preSum出来,preSum[i]记录nums[0..i-1]的累加和: 图片

560. 和为 K 的子数组
前缀和 + 哈希

var subarraySum = function(nums, k) {
   let count = 0
   let map = new Map([[0, 1]])

   const len = nums.length
   let sum = 0
   for (let i =0;i<len;i++) {
       sum += nums[i]
       if (map.has(sum - k)) {
           count += map.get(sum - k)
       }
       if (map.has(sum)) {
           map.set(sum, map.get(sum) + 1)
       } else {
           map.set(sum, 1)
       }
    }
    return count
};

类似的三数之和,是使用排序+双指针

差分数组

核心思路是构造一个diff数组,值为nums[i]-nums[i-1] 905e1632020583e9586bf17333909bb.jpg