基本数据结构

107 阅读6分钟

1 数组

1.1 时间复杂度

数组增删改查的时间复杂度见下表

末尾中间
addo(1)o(1)o(N)o(N)
removeo(1)o(1)o(N)o(N)
seto(1)o(1)o(1)o(1)
geto(1)o(1)o(1)o(1)
  • add元素的时候可能会涉及到数组的扩容操作,但是并不是每次增加元素都会出现扩容,所以扩容的复杂度要用均摊时间复杂度来分析。
  • 操作仅仅适用于给定索引的情况,如果要拿着固定的值去寻找对应的索引,时间复杂度为o(N)o(N)

1.2 动态数组

TODO

2 链表

2.1 链表的优劣

① 优点

  • 不需要连续的内存空间存储元素
  • 不需要考虑扩容和数据迁移问题,理论上链表的容量是无限的

② 缺点

  • 不支持随机访问元素

2.2 单链表的基本操作

① 遍历元素

ListNode p = head;
while (p != null) {
    System.out.println(p.val);
    p = p.next;
}

② 增加元素

// 头插法添加元素
public ListNode addHead(ListNode head, int val) {
    ListNode newNode = new ListNode(val);
    newNode.next = head.next;
    head.next = newNode;
    return head;
}

// 尾插法
public ListNode addTail(ListNode head, int val) {
    ListNode newNode = new ListNode(val);
    ListNode p = head;
    while (p.next != null) {
        p = p.next;
    }
    p.next = newNode;
    return head;
}

// 在指定位置,第 n 个节点后面添加
public ListNode add(ListNode head, int val, int n) {
    ListNode p = head;
    // 找到前驱节点
    for (int i = 0; i < n - 1; i++) {
        p = p.next;
    }
    ListNode node = new ListNode(val);
    node.next = p.next;
    p.next = node;
    return head;
}

③ 删除元素

// 删除第 n 个节点
public ListNode add(ListNode head, int val, int n) { 
	ListNode p = head;
    // 找到第 n个节点的前驱节点
    for (int i = 0; i < n - 1; i++) {
        p = p.next;
    }
    // 将前驱节点的后继指针指向当前节点的后继指针
    p.next = p.next.next;
    return head;
}

2.3 双链表的基本操作

3 队列和栈

3.1 基本概念

  • 队列:先进先出
  • :先进后出

3.2 使用链表来实现队列和栈

3.3 环形数组

环形数组可以利用求模运算,将普通数组变为逻辑上的环形数组,可以让我们使用o(1)o(1)的时间在数组头部增删元素。环形数组的关键在于求模运算%,当索引i到达数组尾部元素时,i + 1arr.length取余会变成0,回到数组头部。逻辑上形成了一个环形数组。

i = (i + 1) % arr.length;

环形数组的关键在于维护了两个指针startendstart指向第一个有效元素,而end指向最后一个有效元素。

区间开闭问题:

环形数组的区间定义为左闭右开的,即[start, end)区间包含数组元素。这样更加有利于处理边界条件。

3.4 数组实现队列和栈

代码略。

3.5 双端队列原理和实现

双端队列:头部和尾部都可以实现插入或者删除元素。

4 哈希表

4.1 基本原理

4.1.1 底层结构

哈希表是通过数组链表|红黑树实现的。

4.1.2 关键概念

① 哈希函数

将输入的key转换为固定长度的输出。

② 哈希冲突

两个不同的key通过哈希函数得到了相同的索引

  • 拉链法
  • 开放地址法

③ 扩容与负载因子

  • 扩容容量:元素数量 > 当前容量 * 0.75 则当前容量 变为 2 倍
  • 负载因子:0.75

Map中的Key必须是不可变的

5 二叉树

二叉树值最重要的数据结构,没有之一

5.1 基本概念

① 完全二叉树

每一层节点都紧凑靠左排列,除了最后一层,其他层都是满二叉树

② 满二叉树

每一层节点都是满的

③ 二叉搜索树

二叉搜索树(Binary Search Tree, BST),对于每个树的节点,其左子树的每个节点的值都要小于父节点的值,右子树的每个节点的值都要大于父节点的值,即左小右大

5.2 二叉树的递归遍历

所谓前序遍历:父节点 -> 左孩子 -> 右孩子

/**
 * 遍历
 * @param root
 * @return
 */
public static final List<Integer> traverse(TreeNode root, Traverse t) {
    List<Integer> resList = new ArrayList<>();
    if (root == null) return null;
    switch (t) {
        case PREFIX: prefixTraverse(root, resList); break;
        case INFIX: infixTraverse(root, resList); break;
        case POSTFIX: postfixTraverse(root, resList); break;
        case LEVEL: levelTraverse(root, resList);
        default: throw new RuntimeException("遍历类型出错");
    }
    return resList;
}

/**
 * 后序遍历
 * @param root
 * @param resList
 */
private static void postfixTraverse(TreeNode root, List<Integer> resList) {
    if (root == null) return;

    postfixTraverse(root.left, resList);

    postfixTraverse(root.right, resList);

    resList.add(root.val);
}

/**
 * 中序遍历
 * @param root
 * @param resList
 */
private static void infixTraverse(TreeNode root, List<Integer> resList) {
    if (root == null) return;

    infixTraverse(root.left, resList);

    resList.add(root.val);

    infixTraverse(root.right, resList);
}

/**
 * 前序遍历
 * @param root
 * @param resList
 */
private static void prefixTraverse(TreeNode root, List<Integer> resList) {
    if (root == null) return;
    resList.add(root.val);
    prefixTraverse(root.left, resList);
    prefixTraverse(root.right, resList);
}


/**
 * 二叉树的层序遍历
 * @param root
 * @param resList
 */
public static void levelTraverse(TreeNode root, List<Integer> resList) {
    if (root == null) return;
    Queue<TreeNode> nodeQueue = new LinkedList<>();
    nodeQueue.add(root);
    while (!nodeQueue.isEmpty()) {
        TreeNode curNode = nodeQueue.poll();
        // 访问当前节点
        resList.add(curNode.val);
        
        // 将当前节点的左右孩子加入队列中
        if (curNode.left != null) {
            nodeQueue.add(curNode.left);
        }
        
        if (curNode.right != null) {
            nodeQueue.add(curNode.right);
        }
    }
}

5.3 多叉树的遍历

多叉树结构是二叉树结构的延伸,多叉树遍历是二叉树遍历的延伸。

多叉树的结构:

class TreeNode {
    int val;
    List<TreeNode> children;
}

① 递归遍历

void traverse (TreeNode root) {
    if (root == null) {
        return;
    }
    
    // 前序遍历
    
    for (int i = 0; i < children.size(); i++) {
        traverse(children[i]);
    }
    
    
    // 后序位置
}

② 层序遍历

void levelTraverse(TreeNode root) {
    if (root == null) return;
    
    Queue<TreeNode> nodeQueue = new LinkedList<>();
    nodeQueue.add(root);
    int depth = 1;
    while (!nodeQueue.isEmpty) {
        int size = nodeQueue.size();
        for (int i = 0; i < size; i++) {
            TreeNode cur = nodeQueue.poll();
            System.out.println("depth: " + depth + ", val: " + cur.val);
            
            for (TreeNode node : cur.children) {
                nodeQueue.add(node);
            }
        }
        depth++;
    }
}

5.4 实现堆排序

顺序存储二叉树:

  • 下标为i的左孩子的下标为2 * i + 1
  • 下标为i的右孩子的下标为2 * i + 2
  • 下标为i的父节点的下标为i / 2 - 1

堆排序原理

  • 从最后一个非叶子节点开始,构建大根堆
  • 逐一将最大元素移动到数组末尾
  • 调整堆结构

构建大根堆的方法

  • 分别定位当前节点,左孩子和右孩子
  • 找出最大值
  • 如果当前节点不是最大值,则和最大值交换
  • 递归调整堆

堆排序实现

public class Heap {
    public static void sort(int[] arr) {

        if (arr == null || arr.length <= 1) return;
        int n = arr.length;

        // 从最后一个非叶子节点构建大顶堆 n / 2 - 1
        for (int i = n / 2 - 1; i >= 0; i--) {
            // 递归构建大根堆
            heapify(arr, n, i);
        }

        // 逐一将最大的元素移动到数组末尾
        for (int i = n - 1; i >= 0; i--) {
            swap(0, i, arr);

            // 调整堆结构, 堆尾索引为 i, 父节点索引为 0
            heapify(arr, i, 0);
        }

    }

    /**
     * 交换数组两个元素的值
     * @param i
     * @param j
     */
    private static void swap(int i, int j, int[] arr) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    /**
     * 调整对结构,使其称为最大堆
     * @param arr
     * @param n 结尾索引
     * @param i 当前节点索引
     */
    private static void heapify(int[] arr, int n, int i) {
        int largest = i; // 默认当前索引值是最大的
        int l = 2 * i + 1;
        int r = 2 * i + 2;

        // 如果左孩子更大
        if (l < n && arr[l] > arr[largest]) {
            largest = l;
        }

        // 如果右孩子大
        if (r < n && arr[r] > arr[largest]) {
            largest = r;
        }

        // 如果最大的不是本节点
        if (largest != i) {
            // 交换当前节点和最大的节点
            swap(largest, i, arr);

            // 调整堆结构,从某个孩子开始,调整为最大堆
            heapify(arr, n, largest);
        }

    }
}