计算机基础学习笔记 | 数据结构基础

数据结构

    • 学习资料
    • 基础
      • 十种常用数据结构
      • 十种常用的算法
      • 时间复杂度
      • 空间复杂度
    • 基础数据结构
      • 数组 array
          • 读取元素
          • 更新元素
          • 插入元素
          • 删除元素
      • 链表 (linked list)
          • 查找节点
          • 更新节点
          • 插入节点
          • 删除节点
        • 数组和链表的对比
    • 逻辑结构
      • 队列
      • 散列表(哈希表)
          • 写操作
          • 写操作
          • 扩容
      • 应用
        • 二叉树
          • 应用
          • 二叉树的遍历
        • 二叉堆
          • 二叉堆的应用:优先队列
        • 树知识点小节

学习资料

  • 极客时间:数据结构与算法之美
  • 《小灰的漫画算法之旅》

基础

  • 数据结构:数据的组织、管理、存储格式,其目的是为了高效的访问和修改数据
  • 算法:一系列程序指令,用于处理特定的运算和逻辑问题

十种常用数据结构

  • 数组
  • 链表
  • 队列
  • 散列表
  • 二叉树
  • 跳表
  • Trie 树

十种常用的算法

  • 递归
  • 排序
  • 二分查找
  • 搜索
  • 哈希算法
  • 贪心算法
  • 分治算法
  • 回溯算法
  • 动态规划
  • 字符串匹配算法

时间复杂度

网图,侵权请联系删除
计算机基础学习笔记 | 数据结构基础_第1张图片
大O表达法,用来大概表示需要进行的时间

  • 忽略低阶、常量、系数三部分并不左右增长趋势
    几个方法:
  • 之关系循环次数最多的一段代码
  • 加法法则:总复杂度等于量级最大的那段代码的复杂度
  • 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

常见时间复杂度:

  • 多项式量级
  • 非多项式量级(O(2^n) 和 O(n!) )

多项式时间复杂度

  • O(1)
int i = 1;
int j = 2;
int sum = i + j;
  • O(logn)
    i = 1;
    while(i <= n){
        i = i * 2;
    }

2^x = n,则 x = log2(n),忽略底数O(logn)

  • O(m + n)、O( m * n)

由多个数据规模来决定时间复杂度:不能确定m、n的值,则为 O(m + n)


int cal(int m, int n) {
  int sum_1 = 0;
  int i = 1;
  for (; i < m; ++i) {
    sum_1 = sum_1 + i;
  }

  int sum_2 = 0;
  int j = 1;
  for (; j < n; ++j) {
    sum_2 = sum_2 + j;
  }

  return sum_1 + sum_2;
}

空间复杂度

常见为O(1)、O(n)、O(n^2)

  • 常量空间O(1):算法的存储空间大小固定,和输入规模无直接关系
  • 线性空间O(n):线性集合(数组),且集合大小和输入规模n成正比
  • 二维空间O(n^2):二维数组
  • 递归空间:与递归深度成正比

基础数据结构

数组 array

在内存中顺序存储(占用一片连续的内存地址);每个元素有着自己的下标,可以通过下标查找元素;

  • 数组插入、删除的时间复杂度:O(n)
  • 数组查找、更新的时间复杂度:O(1)
  • 优势:查找效率高,只需要给出下标
  • 劣势:插入、删除效率低,需要移动大量元素
读取元素

因为数组在内存中顺序存储,所以可以直接通过下标读取到对应的数组元素 ,这种读取元素的方式叫做随机读取

 int[] array = new int[]{3,1,2,5,4,9,7,2}; 
 // 输出数组中下标为3的元素 
 System.out.println(array[3]);
更新元素

直接通过下标赋值

 int[] array = new int[]{3,1,2,5,4,9,7,2}; 
 // 给数组下标为5的元素赋值 
 array[5] = 10; 
 // 输出数组中下标为5的元素 
 System.out.println(array[5])
插入元素

存在三种情况

  • 尾部插入
  • 中间插入
  • 超范围插入

尾部插入

直接插入到尾部空闲位置,等同于更新元素

中间插入

先把插入元素以及后面的元素向后移动,再将要插入的元素放到对应的数据位置

超范围插入

需要进行数组扩容:创建一个新的数组,再将旧数组的元素复制过去

删除元素

将元素逐个向左移位

链表 (linked list)

链表是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成,在内存中的存储方式是随机存储;
单向链表的每一节点又包含两部分,一部分存放数据data,一部分是指向下一个节点的指针next;第一个节点成为头节点,最后一个节点称为尾节点
双向列表不仅拥有data、next部分,还存放指向前置节点的prev指针

查找节点

只能根据头节点开始向后一个一个节点逐一查找,时间复杂度:最坏的情况是O(n)

更新节点

如果不考虑查找节点的过程,链表更新直接替换新数据即可,时间复杂度O(n)

插入节点
  • 尾部插入:把最后一个节点的next指针指向新插入的节点即可
  • 头部插入:1、把新节点的next指向原头结点;2、把新节点变为链表的头节点
  • 中间插入:1、新节点的next指向插入位置的节点;2、插入位置的前一个节点指向新节点
删除节点
  • 尾部删除:尾节点直接指向空
  • 头部删除:将头节点指向原头节点的next指针
  • 中间删除:将要删除节点的前置节点指向要删除节点的下一个节点

数组和链表的对比

查找 更新 插入 删除
数组 O(1) O(1) O(n) O(n)
链表 O(n) O(1) O(1) O(1)
  • 数组适合读取操作多、写操作少的场景
  • 链表适合插入、删除多的情况
  • 数组和链表都属于“物理结构”,是存在的存储结构;与之相对应的是逻辑结构,是抽象、依赖物理结构存在的

逻辑结构

  • 队列 (就像一个不封底的兵乓球桶,)
  • 散列表

  • 就像一个封底的乒乓球桶,先放进去的后拿出来,即“先进后出”
  • 可以用数组或者链表实现
  • 入栈(push):只允许栈顶一侧入栈,时间复杂度:O(1)
  • 出栈(pop):只允许栈顶元素出栈,时间复杂度:O(1)

队列

  • 就像隧道,通过隧道的车辆只能从一边出、一边入,并且先驶入的先出来,不能“超车”,也不能“逆行”
  • 可以用数组或者链表实现
  • 入队(enqueue):只允许在队尾位置放入元素
  • 出队(dequeue):只允许在队头一侧移除元素
  • 循环队列:使数组形式存在的队列,在不断的出队入队中维持队列容量的恒定;具体操作:当队列满的时候,队尾指针指向数组的首位,直到(队尾指针+1)%数组长度 = 队头下标表示队列真的存满了

散列表(哈希表)

  • 存在 键-值的映射关系(Key-Vaule),时间复杂度接近于O(1)
  • 本质上也是数组,通过哈希函数将Key转换成对应的下标
  • 通过 开放寻址法链表法来解决哈希冲突
写操作
  • 通过哈希函数将key值转换为下标
  • 如果下标无元素,则将元素填充到该下标;如果该下标下已经有元素了(哈希冲突),则使用开发寻址法(寻找下一个空档位置)或者链表法(将原元素的next下标指向要添加的元素)
写操作
  • 通过哈希函数,将key转化为数组下标
  • 通过这个下标找到对应的元素,再通过链表一个个比对key值是否相等
扩容
  • 创建一个长度为原数组两倍的新的空数组
  • 遍历所有元素,重新Hash后,添加到新数组中

应用

  • 栈的应用:递归、回溯历史(回退栈)
  • 队列的应用:对历史的“回放”

例如在多线程中,争夺公平锁的等待队列,就是按照访问顺序来决定线程在队列中的
次序的。

  • 双端队列:可以在队头的一端入队或出队,也可以从队尾的一端入队或出队
  • 优先队列:优先级高的节点先出队
  • 散列表代表:HashMap

  1. 有且仅有一个特定的称为根的节点。
  2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一 个树,并称为根的子树

计算机基础学习笔记 | 数据结构基础_第2张图片
计算机基础学习笔记 | 数据结构基础_第3张图片

  • 节点1是根节点(root),节点5、6、7、8是树的末端,没有“孩子”,被 称为叶子节点(leaf)。图中的虚线部分,是根节点1的其中一个子树。
  • 节点4的上一级节点,是节点4的父节点(parent);从节点4衍生出来的 节点,是节点4的孩子节点(child);和节点4同级,由同一个父节点衍生出来的节点, 是节点4的兄弟节点(sibling)
  • 树的最大层级数,被称为树的高度或深度。显然,上图这个树的高度是4。

二叉树

  • 是树的一种特殊的形式
  • 每个节点最多(0、1、2)有两个子节点(左孩子、右孩子)
  • 满二叉树定义:一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上, 那么这个树就是满二叉树
  • 完全二叉树:对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这 个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完 全二叉树。

满二叉树和完全二叉树的区别:满二叉树要求所有分支都是满的;而完全 二叉树只需保证最后一个节点之前的节点都齐全即可

链表实现

  • 存储数据的data变量
  • 指向左孩子的left指针
  • 指向右孩子的right指针

数组实现
计算机基础学习笔记 | 数据结构基础_第4张图片
当子孩子没有数据时数组相应的位置会空出来,可以方便计算节点位置

  • 当一个父节点下标是parent,则左孩子下标为:2 * parent + 1;右孩子下标为:2 * parent + 2
  • 如果一个左孩子的下标是leftChild,则父节点下标位 (leftChild - 1)/ 2
应用
  • 二叉查找树
  • 也叫二叉排序树,特点:
  • 如果左子树不为空,则左子树上所有节点的值均小于根节点的值
  • 如果右子树不为空,则右子树上所有节点的值均大于根节点的值
  • 左、右子树也都是二叉查找树
    自平衡:
    特殊情况下,会导致“失衡”,解决方法:自平衡(红黑树、AVL树、树堆)
二叉树的遍历
  • 前序遍历:输出顺序 根节点 -> 左子树 -> 右子树
  • 中序遍历:输出顺序 左子树 -> 根节点-> 右子树
  • 后序遍历:输出顺序 左子树 -> 右子树 -> 根节点
  • 层序遍历(广度优先遍历,一层层遍历)
 /**
     * 按前序遍历的顺序构建二叉树
     * @param inputList
     * @return
     */
    public static TreeNode createBinaryTree(LinkedList<Integer> inputList){

        if (inputList == null || inputList.isEmpty()) return null;

        TreeNode node = null;
        Integer data = inputList.removeFirst();
        if (data != null){
            node = new TreeNode(data);
            node.leftNode = createBinaryTree(inputList);
            node.rightNode = createBinaryTree(inputList);
        }


        return node;

    }

    /**
     * 二叉树的前序遍历
     * @param node
     */
    public static void preOrderTraveral(TreeNode node){

        if (node == null) return;

        System.out.print(node.data);
        preOrderTraveral(node.leftNode);
        preOrderTraveral(node.rightNode);

    }

    /**
     * 二叉树的中序遍历
     * @param node
     */
    public static void inOrderTraveral(TreeNode node){
        if (node == null) return;

        inOrderTraveral(node.leftNode);
        System.out.print(node.data);
        inOrderTraveral(node.rightNode);
    }

    /**
     * 二叉树的后序遍历
     * @param node
     */
    public static void postOrderTraveral(TreeNode node){
        if (node == null) return;

        postOrderTraveral(node.leftNode);
        postOrderTraveral(node.rightNode);
        System.out.print(node.data);
    }

二叉堆

本质上是一种完全二叉树,有两种类型:1. 最大堆 2.最小堆
最大堆:任何一个父节点的值,都大于或等于它左、右孩子节点 的值。
最小堆:的任何一个父节点的值,都小于或等于它左、右孩子节点的值。
两类操作:“上浮”和下沉
操作:

  • 删除:是单一节点的下沉,时间复杂度O(logn)
  • 插入:是单一节点的上浮,时间复杂度O(logn)
  • 构建:需要所有非叶子节点依次下沉,时间复杂度O(n)

应用:

  • 实现优先队列
  • 堆排序
     /**
     * 堆的上浮操作
     * @param array 插入新数据后未调整的堆
     */
    public static void upAdjust(int[] array){
        int childIndex = array.length - 1;
        int parentIndex = (childIndex - 1)/2; // 找到父节点

        int temp = array[childIndex]; // temp 保存插入的叶子节点值,用于最后的赋值

        while (childIndex > 0 && temp < array[parentIndex]){
            array[childIndex] = array[parentIndex];
            childIndex = parentIndex;
            parentIndex = (childIndex - 1)/2;
        }

        array[childIndex] = temp;

    }


    /**
     * 堆的下沉操作
     * @param array 待调整的堆
     * @param parentIndex 要“下沉”的父节点
     * @param length 堆的有效长度
     */
    public static void downAdjust(int[] array,int parentIndex,int length){

        int temp = array[parentIndex];
        int childIndex = 2 * parentIndex + 1; // 找到左孩子

        while (childIndex < length){

            // 如果存在右孩子,且右孩子比左孩子小,将指针指向右孩子
            if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]){
                childIndex++;
            }

            // 如果父节点小于两个子孩子的值,则跳出
            if (temp < array[childIndex]) break;

            array[parentIndex] = array[childIndex];
            parentIndex = childIndex;
            childIndex = 2 * parentIndex + 1;

        }

        array[parentIndex] = temp;
    }


    public static void buildHeap(int[] array){

        // 从最后一个非叶子节点开始,依次做“下沉”调整
        for (int i = (array.length - 2)/2;i >= 0;i--){
            downAdjust(array,i,array.length);
        }

    }
二叉堆的应用:优先队列

队列遵循先进先出(FIFO)原则,优先队列不再遵循先进先出的原则,而是分为两种情况:

  • 最大优先队列,无论入队顺序如何,都是当前最大的元素优先出队
  • 最小优先队列,无论入队顺序如何,都是当前最小的元素优先出

特性:
入队:在数组末插入新节点,让新节点“上浮”到合适的位置,时间复杂度:O(logn)
出队:将堆顶的元素出栈,再将最后一个元素移到对顶,再进行“下沉”操作,时间复杂度:O(logn)

  private int[] array;
    private int size; // 当前队列大小


    public PriorityQueue() {

        // 初始长度为 32
        array = new int[32];

    }


    /**
     * 入队
     * @param val
     */
    public void enqueue(int val){
        if (size > array.length) resize();

        array[size++] = val;
        HeapHelper.upAdjust(array,size); // 上浮调整,传入有效长度
    }

    /**
     * 出队
     * @return
     * @throws Exception
     */
    public int dequeue() throws Exception {
        if (size <= 0) throw new Exception("no more data");
        int head = array[0];
        array[0] = array[--size];
        HeapHelper.downAdjust(array,0,size);// 0:要下沉的节点,这里是第一个,size:有效长度
        return head;

    }

树知识点小节

这里直接搬书里的

  • 什么是树
    树是n个节点的有限集,有且仅有一个特定的称为根的节点。当n>1时,其余节点可 分为m个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
  • 什么是二叉树
    二叉树是树的一种特殊形式,每一个节点最多有两个孩子节点。二叉树包含完全二叉 树和满二叉树两种特殊形式。
  • 二叉树的遍历方式有几种
    根据遍历节点之间的关系,可以分为前序遍历、中序遍历、后序遍历、层序遍历这4 种方式;从更宏观的角度划分,可以划分为深度优先遍历和广度优先遍历两大类。
  • 什么是二叉堆
    二叉堆是一种特殊的完全二叉树,分为最大堆和最小堆。
    在最大堆中,任何一个父节点的值,都大于或等于它左、右孩子节点的值。
    在最小堆中,任何一个父节点的值,都小于或等于它左、右孩子节点的值。
  • 什么是优先队列
    优先队列分为最大优先队列和最小优先队列。
    在最大优先队列中,无论入队顺序如何,当前最大的元素都会优先出队,这是基于最 大堆实现的。
    在最小优先队列中,无论入队顺序如何,当前最小的元素都会优先出队,这是基于最 小堆实现的。

你可能感兴趣的:(计算机基础学习)