一、数据结构
1、线性结构
- 数组:
- 访问:O(1)访问特定位置的元素;
- 插入:O(n)最坏的情况发生在插入发生在数组的首部并需要移动所有元素时;
- 删除:O(n)最坏的情况发生在删除数组的开头发生并需要移动第一元素后面所有的元素时
- 链表:使用的不是连续的内存空间来存储数据。
- 链表的插入和删除操作的复杂度为 O(1) ,只需要知道目标位置元素的上一个元素即可。但是,在查找一个节点或者访问特定位置的节点的时候复杂度为 O(n) 。
- 数组链表比较:
- 数组支持随机访问,而链表不支持。
- 数组使用的是连续内存空间对 CPU 的缓存机制友好,链表则相反。
- 数组的大小固定,而链表则天然支持动态扩容。如果声明的数组过小,需要另外申请一个更大的内存空间存放数组元素,然后将原数组拷贝进去,这个操作是比较耗时的
- 栈
- 允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop),后进先出(LIFO, Last In First Out) ,push 和 pop 的操作都发生在栈顶。
- 队列
- 先进先出 (FIFO,First In, First Out) 的线性表。通常用链表或者数组来实现,用数组实现的队列叫作 顺序队列 ,用链表实现的队列叫作 链式队列 。队列只允许在后端(rear)进行插入操作也就是入队 enqueue,在前端(front)进行删除操作也就是出队 dequeue
2、图
- 概念
- 顶点:图中的数据元素,图至少有一个顶点(非空有穷集合)
- 边:顶点之间的关系用边表示
- 度:表示一个顶点包含多少条边,在有向图中,还分为出度和入度,出度表示从该顶点出去的边的条数,入度表示进入该顶点的边的条数。
- 无向图、有向图:边表示的是顶点之间的关系,有的关系是双向的,有的是单向的
- 无权图、带权图:边是否具有权重
- 图的存储
-
邻接矩阵存储:邻接矩阵将图用二维矩阵存储,是一种较为直观的表示方式。如果第 i 个顶点和第 j 个顶点之间有关系,且关系权值为 n,则
A[i][j]=n
。在无向图中,当顶点 i 和顶点 j 有关系时,A[i][j]
=1,无关系时,A[i][j]
=0。(比较浪费空间)- 无向图的邻接矩阵是一个对称矩阵,因为在无向图中,顶点 i 和顶点 j 有关系,则顶点 j 和顶点 i 必有关系。
-
邻接表存储:使用一个链表来存储某个顶点的所有后继相邻顶点。对于图中每个顶点 Vi,把所有邻接于 Vi 的顶点 Vj 链成一个单链表,这个单链表称为顶点 Vi 的 邻接表 。
- 在无向图中,邻接表元素个数等于边的条数的两倍;在有向图中,邻接表元素个数等于边的条数
-
- 图的搜索:BFS、DFS
3、堆
-
定义:堆是一种满足以下条件的树:堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值。(可以看成是近似的完全二叉树)
-
用途:当我们只关心所有数据中的最大值或者最小值,存在多次获取最大值或者最小值,多次插入或删除数据时,就可以使用堆。
-
时间复杂度:插入和删除操作O(lgn),堆初始化时间复杂度O(n)
-
分类:大根堆、小根堆
-
存储:由于完全二叉树的性质,利用数组存储二叉树即节省空间,又方便索引(若根结点的序号为0,那么对于树中任意节点 i,其左子节点序号为
2*i + 1
,右子节点序号为2*i+2
) -
堆的操作(大根堆):
-
插入:先将元素放至数组末尾,再自底向上堆化,将末尾元素上浮
-
删除堆顶:删除堆顶元素,将末尾元素放至堆顶,再自顶向下堆化,将堆顶元素下沉
-
-
堆排序:
-
第一步是建堆,将一个无序的数组建立为一个堆
- 最后一个节点的父结点及它之前的元素,都是非叶节点。即节点个数为 n,只需对 (n - 1) / 2 到 0 的节点进行自顶向下(沉底)堆化(顺序是从后往前堆化)
-
第二步是排序,将堆顶元素与最后位置元素交换,然后对剩下元素进行堆化,反复迭代
-
java
堆以及堆排序
class Solution {
public int[] sortArray(int[] nums) {
//若根结点的序号为0,那么对于树中任意节点 i,其左子节点序号为 2*i + 1,右子节点序号为 2*i+2
//初始堆化:从(n - 1)/2 到0,自顶向下堆化
heapify(nums);
//将堆顶元素与最后位置元素交换,然后对0位置进行堆化,反复迭代
for (int j = nums.length - 1; j > 0; j--) {
swap(j, 0, nums);
siftDown(0, j, nums);
}
return nums;
}
private void heapify(int[] nums) {
for (int i = (nums.length >>> 1) - 1; i >= 0; i--) {
siftDown(i, nums.length, nums);
}
}
//大根堆
private void siftDown(int index, int len, int[] nums) {
int half = len >>> 1;
while (index < half) {
int child = (index << 1) + 1;//左移运算符优先级低于加号
int right = 1 + child;
//获取左右节点中较大的子节点
if (right < len && nums[child] < nums[right]) {
child = right;
}
if (nums[index] >= nums[child]) {
break;
}
//跟较大的子节点交换
swap(index, child, nums);
index = child;
}
}
private void siftUp(int index, int[] nums) {
while (index > 0) {
int father = (index - 1) >>> 1;
if (nums[father] > nums[index]) {
break;
}
swap(father, index, nums);
index = father;
}
}
private void swap(int a, int b, int[] nums) {
nums[a] ^= nums[b];
nums[b] ^= nums[a];
nums[a] ^= nums[b];
}
}
4、树
- 定义
- 二叉树分类
-
满二叉树:每一个层的结点数都达到最大值。或者如果一个二叉树的层数为 K,且结点总数是(2^k) -1 ,则它就是 满二叉树
-
完全二叉树:最后一层外,若其余层都是满的,并且最后一层是满的或者是在右边缺少连续若干节点。当根节点的值为 1 的情况下,若父结点的序号是 i,那么左子节点的序号就是 2i,右子节点的序号是 2i+1。
-
平衡二叉树:可以是一棵空树;如果不是空树,它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。(红黑树、AVL数)
-
- 存储
-
链式存储:
-
数组存储:顺序存储就是利用数组进行存储,数组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。根结点的序号为 0,对于每个节点 Node,假设它存储在数组中下标为 i 的位置,那么它的左子节点就存储在 2i + 1 的位置,它的右子节点存储在下标为 2i+2 的位置。
- 如果存储的二叉树不是完全二叉树,在数组中就会出现空隙,导致内存利用率降低
-
- 遍历
-
层序遍历:bfs
-
先序遍历
-
中序遍历
-
后序遍历
-
morris遍历
-
5、红黑树
二、算法
1、算法思想
- 贪心:局部最优-->全局最优
- 动态规划:动态规划中每一个状态一定是由上一个状态推导出来
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 回溯-穷举
- 回溯函数返回值以及参数
- 回溯函数终止条件
- 回溯函数终止条件
- 分治
- 将一个规模为 N 的问题分解为 K 个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。