数构&算法:数据结构
- 数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关,以下是各种数据结构的详细说明。
线性结构:数组、队列、链表、栈
顺序存储(地址连续)
链式存储(地址不一定连续)
非线性结构:二维数组、多维数组、广义表、树、图
①数组
❶稀疏数组
- 稀疏数组是一种用来压缩数据量的数据结构,简而言之,就是记录特殊值,然后剩下大量重复的数据可以消减。
例如下方是一个普通二维数组
0 0 0 0 0 0
0 0 1 0 0 0
0 0 0 2 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
这么一个二维数组,化成稀疏数组可以表示为:
行 列 值
0 6 6 2
1 1 2 1
2 2 3 2
1. 稀疏数组第一行表示原数组有多少行,多少列,有多少个非零元素(有效值)
2. 稀疏数组是从0开始的
3. 稀疏数组的行数等于有效值+1,列数固定都为3
二维数组转稀疏数组的步骤:
- 遍历二维数组,得到有效值个数 sum
- 根据 sum 创建稀疏数组 sparseArr = int [sum+1][3]
- 将有效值存入稀疏数组
还原稀疏数组步骤:
- 创建一个新的数组,其行和列等于稀疏数组首行数据
- 遍历稀疏数组,将对应数值赋值给新的数组
- 最后可以验证一下原始的数组和还原后的数组是否相等
//稀疏数组:用来减少数据量
public class SparseArray {
public static void main(String[] args) {
// 一、构建原始数组
// 创建一个二维数组6*6 0:没有棋子,1:黑棋 2:白棋
int[][] chessArr = new int[6][6];
chessArr[1][2] = 1;
chessArr[2][3] = 2;
System.out.println("原始数组:");
for (int[] row : chessArr) {
for (int data : row) {
System.out.print(data+"t");
}
System.out.println();
}
System.out.println("====================");
// 二、转换成稀疏数组
int sum = 0;
//1.先遍历二维数组,获取有效值的个数
for (int i = 0; i
❷数组模拟队列
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图
maxSize 是该队列的最大容量,两个变量 front 及 rear 分别记录队列前后端的下标
class ArrayQueue {
private服务器托管网 int MaxSize; // 队列大小
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 数组存放数据
// 一、创建队列的构造器
public ArrayQueue(int MaxSize) {
this.MaxSize = MaxSize;
arr = new int[this.MaxSize];
front = -1;
rear = -1;
}
//二、判断队列是否满
public boolean isFull() {
return rear == MaxSize - 1;
}
//三、判断队列是否空
public boolean isEmpty() {
return rear == front;
}
//四、入队
public void addQueue(int num) {
if (isFull()) {
System.out.println("队列已满,无法在进行入队操作");
return;
}
arr[++rear] = num;
}
//五、出队
public int getQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法出队");
}
return arr[++front];
}
//六、显示队列数据
public void showQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法遍历");
}
for (int i = front+1; i
测试
public class ArrayQueueDemo {
public static void main(String[] args) {
// 构造队列
ArrayQueue queue = new ArrayQueue(5);
// 入队
queue.addQueue(1);
queue.addQueue(2);
queue.addQueue(3);
queue.addQueue(4);
queue.addQueue(5);
// 出队
System.out.println(queue.getQueue());
// 遍历队列
queue.showQueue();
// 队首
System.out.println(queue.headQueue());
}
}
②链表
❶单向链表
特点
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域 (存储数据),next 域(指向下一个节点)
- 链表的各个节点不一定是连续存储的
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
/**
* 定义节点
*/
class StudentNode {
int id;
String name;
StudentNode next;
public StudentNode(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "StudentNode{" +
"id=" + id +
", name='" + name + ''' +
'}';
}
}
/**
* 创建链表
*/
class singleLinkedList {
//头节点,防止被修改,设置为私有的
private StudentNode head = new StudentNode(0, "");
//插入节点
public void addNode(StudentNode node) {
//因为头节点不能被修改,所以创建一个辅助节点
StudentNode temp = head;
//找到最后一个节点
while (temp.next != null) {
temp = temp.next;
}
temp.next = node;
}
//按id顺序插入节点
public void addByOrder(StudentNode node) {
//如果没有首节点,就直接插入
if (head.next == null) {
head.next = node;
return;
}
//辅助节点,用于找到插入位置和插入操作
StudentNode temp = head;
//节点的下一个节点存在,且它的id小于要插入节点的id,就继续下移
while (temp.next != null && temp.next.id length) {
throw new RuntimeException("链表越界");
}
temp = head;
for (int i = 0; i stack = new Stack();
while (temp.next != null) {
stack.push(temp.next);
temp = temp.next;
}
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
}
}
public class SingleLinkedListDemo {
public static void main(String[] args) {
singleLinkedList linkedList = new singleLinkedList();
//创建学生节点,并插入链表
System.out.println("插入节点1和3:");
StudentNode student1 = new StudentNode(1, "Jack");
StudentNode student3 = new StudentNode(3, "Tom");
linkedList.addNode(student1);
linkedList.addNode(student3);
linkedList.traverseNode();
//按id大小插入
System.out.println("有序插入节点2:");
StudentNode student2 = new StudentNode(2, "Jerry");
linkedList.addByOrder(student2);
linkedList.traverseNode();
//按id修改学生信息
System.out.println("修改节点1信息:");
student2 = new StudentNode(1, "Jack2");
linkedList.changeNode(student2);
linkedList.traverseNode();
//获得第2个节点
System.out.println("获得第2个节点:");
System.out.println(linkedList.getNodeByIndex(2));
//根据id删除学生信息
System.out.println("删除学生信息:");
student2 = new StudentNode(1, "Jack2");
linkedList.deleteNode(student2);
linkedList.traverseNode();
//倒叙遍历链表
System.out.println("倒序遍历链表:");
linkedList.reverseTraverse();
}
}
链表为空
插入节点1和3:
StudentNode{id=1, name='Jack'}
StudentNode{id=3, name='Tom'}
有序插入节点2:
StudentNode{id=1, name='Jack'}
StudentNode{id=2, name='Jerry'}
StudentNode{id=3, name='Tom'}
修改节点1信息:
StudentNode{id=1, name='Jack2'}
StudentNode{id=2, name='Jerry'}
StudentNode{id=3, name='Tom'}
获得第2个节点:
StudentNode{id=2, name='Jerry'}
删除学生信息:
StudentNode{id=2, name='Jerry'}
StudentNode{id=3, name='Tom'}
倒序遍历链表:
StudentNode{id=3, name='Tom'}
StudentNode{id=2, name='Jerry'}
❷双向链表
/**
* 定义节点
*/
class HeroNode {
int id;
String name;
HeroNode next;
HeroNode pre;
public HeroNode(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{id=" + id + ", name=" + name + "}";
}
}
/**
* 创建一个双向链表的类
*/
class DoubleLinkedList {
//初始化一个头节点,头节点不动,不存放具体的数据
HeroNode head = new HeroNode(0, "");
//初始化一个尾节点,指向最后一个元素,默认等于head
HeroNode tail = head;
//遍历打印双向链表的方法
public void list() {
if (head.next == null) {
System.out.println("链表为空");
return;
}
HeroNode temp = head.next;
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
//新增节点
public void add(HeroNode heroNode) {
tail.next = heroNode;
heroNode.pre = tail;
tail = heroNode;
}
//有序新增节点
public void addByOrder(HeroNode heroNode) {
HeroNode temp = head;
// 标记添加的编号是否已经存在
boolean flag = false;
while (temp.next != null && temp.next.id
双向链表:
HeroNode{id=1, name=宋江}
HeroNode{id=2, name=卢俊义}
HeroNode{id=3, name=吴用}
HeroNode{id=4, name=林冲}
修改节点4:
HeroNode{id=1, name=宋江}
HeroNode{id=2, name=卢俊义}
HeroNode{id=3, name=吴用}
HeroNode{id=4, name=公孙胜}
删除节点3
HeroNode{id=1, name=宋江}
HeroNode{id=2, name=卢俊义}
HeroNode{id=4, name=公孙胜}
测试有序增加链表:
英雄编号【2】已经存在了
英雄编号【4】已经存在了
英雄编号【2】已经存在了
HeroNode{id=1, name=宋江}
HeroNode{id=2, name=卢俊义}
HeroNode{id=3, name=吴用}
HeroNode{id=4, name=公孙胜}
❸循环链表
③栈&队列&堆
❶普通队列-Queue
队列是一种先进先出的数据结构,元素从后端入队,然后从前端出队。
Queue queue = new LinkedList();
常用方法
函数 | 功能 |
---|---|
add(E e)/offer(E e) | 压入元素 |
remove()/poll() | 弹出元素 |
element()/peek() | 获取队头元素 |
isEmpty() | 用于检查此队列是“空”还是“非空” |
size() | 获取队列长度 |
❷双端队列-Deque
Java集合提供了接口Deque
来实现一个双端队列,它的功能是:
- 既可以添加到队尾,也可以添加到队首;
- 既可以从队首获取,又可以从队尾获取。
Deque有三种用途
- 普通队列(一端进另一端出):
Deque queue = new LinkedList();
// 等价
Queue queue = new LinkedList();
Queue方法 | 等效Deque方法 |
---|---|
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
- 双端队列(两端都可进出)
//底层:ArrayDeque(动态数组)和 LinkedList(链表)
Deque deque = new ArrayDeque();
Deque deque = new LinkedList();
第一个元素 (头部) | 最后一个元素 (尾部) | |
---|---|---|
插入 | addFirst(e)/offerFirst(e) | addLast(e)/offerLast(e) |
删除 | removeFirst()/pollFirst() | removeLast()/pollLast() |
获取 | getFirst()/peekFirst() | getLast()/peekLast() |
- 堆栈(先进后出)
//底层:ArrayDeque(动态数组)和 LinkedList(链表)
Deque stack = new LinkedList();
Deque stack = new ArrayDeque(); //速度更快
// 等价
Stack stack=new Stack();
堆栈方法 | 等效Deque方法 |
---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
Deque所有方法
方法 | 描述 |
---|---|
添加功能 | |
push(E) | 向队列头部插入一个元素,失败时抛出异常 |
addFirst(E) | 向队列头部插入一个元素,失败时抛出异常 |
addLast(E) | 向队列尾部插入一个元素,失败时抛出异常 |
offerFirst(E) | 向队列头部加入一个元素,失败时返回false |
offerLast(E) | 向队列尾部加入一个元素,失败时返回false |
获取功能 | |
peek() | 获取队列头部元素,队列为空时抛出异常 |
getFirst() | 获取队列头部元素,队列为空时抛出异常 |
getLast() | 获取队列尾部元素,队列为空时抛出异常 |
peekFirst() | 获取队列头部元素,队列为空时返回null |
peekLast() | 获取队列尾部元素,队列为空时返回null |
删除功能 | |
removeFirstOccurrence(Object) | 删除第一次出现的指定元素,不存在时返回false |
removeLastOccurrence(Object) | 删除最后一次出现的指定元素,不存在时返回false |
弹出功能 | |
pop() | 弹出队列头部元素,队列为空时抛出异常 |
removeFirst() | 弹出队列头部元素,队列为空时抛出异常 |
removeLast() | 弹出队列尾部元素,队列为空时抛出异常 |
pollFirst() | 弹出队列头部元素,队列为空时返回null |
pollLast() | 弹出队列尾部元素,队列为空时返回null |
❸优先队列-PriorityQueue
优先级队列其实就是一个披着队列外衣的堆,因为优先队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
PriorityQueue 是具有优先级别的队列,优先级队列的元素按照它们的自然顺序排序,或者由队列构造时提供的 Comparator 进行排序,这取决于使用的是哪个构造函数
构造函数 | 描述 |
---|---|
PriorityQueue() | 使用默认的容量(11)创建一个优队列,元素的顺序规则采用的是自然顺序 |
PriorityQueue(int initialCapacity) | 使用默认指定的容量创建一个优队列,元素的顺序规则采用的是自然顺序 |
PriorityQueue(Comparator super E> comparator) | 使用默认的容量队列,元素的顺序规则采用的是 comparator |
//默认按自然顺序(升序)检索的
PriorityQueue numbers = new PriorityQueue();
PriorityQueue numbers = new PriorityQueue(3); //大小为3
//使用Comparator接口自定义此顺序
PriorityQueue queue = new PriorityQueue(new Comparator() {
public int compare(int[] m, int[] n) {
return m[1] - n[1];
}
});
常用方法
peek()//返回队首元素
poll()//返回队首元素,队首元素出队列
add()//添加元素
size()//返回队列元素个数
isEmpty()//判断队列是否为空,为空返回true,不空返回false
❹栈-Stack/Deque
栈是一种后进先出的数据结构,元素从顶端入栈,然后从顶端出栈。
注意:Java 堆栈 Stack 类已经过时,Java 官方推荐使用 Deque 替代 Stack 使用。Deque 堆栈操作方法:push()、pop()、peek()。
创建栈
//方法一,弃用
Stack stack=new Stack();
Stack stack=new Stack();
//方法二:推荐使用
//底层:ArrayDeque(动态数组)和 LinkedList(链表)
Deque stack = new ArrayDeque();
Deque stack = new LinkedList();
stack.push("a");
stack.pop();
stack.push("b");
System.out.println(stack);
常用方法
函数 | 功能 |
---|---|
push(T t) | 压栈(向栈顶放入元素) |
pop() | 出栈(拿出栈顶元素,并得到它的值) |
peek() | 将栈顶元素返回,但是不从栈中移除它 |
search(Object o) | 返回对象在此堆栈上的从1开始的位置。 |
isEmpty() | 判断栈是否为空 |
size() | 获取栈长度 |
❺堆-Heap
堆通常可以被看做是一棵完全二叉树的数组对象。
堆的特性:
- 1.堆是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。
- 2.堆通常用数组来实现。将二叉树的结点按照层级顺序放入数组中,根结点在位置1,它的子结点在位置2和3,而子结点的子结点则分别在位置4,5,6和7,以此类推。(0被废弃)
如果一个结点的位置为k
,则它的父结点的位置为[k/2]
,而它的两个子结点的位置则分别为2k
和2k+1
。
这样,在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动:从a[k]向上一层,就令k等于k/2,向下一层就令k等于2k或2k+1。
- 3.每个结点都大于等于它的两个子结点。这里要注意堆中仅仅规定了每个结点大于等于它的两个子结点,但这两个子结点的顺序并没有做规定,跟我们之前学习的二叉查找树是有区别的。
堆的API设计
public class Heap> {
//存储堆中的元素
private T[] items;
//记录堆中元素的个数
private int N;
public Heap(int capacity) {
this.items = (T[]) new Comparable[capacity + 1];
this.N = 0;
}
//判断堆中索引i处的元素是否小于索引j处的元素
private boolean less(int i, int j) {
return items[i].compareTo(items[j]) 1) {
//比较当前结点和其父结点
if (less(k / 2, k)) {
exch(k / 2, k);
}
k = k / 2;
}
}
//删除堆中最大的元素,并返回这个最大元素
public T delMax() {
T max = items[1];
//交换索引1处的元素和最大索引处的元素,让完全二叉树中最右侧的元素变为临时根结点
exch(1, N);
//最大索引处的元素删除掉
items[N] = null;服务器托管网
//元素个数-1
N--;
//通过下沉调整堆,让堆重新有序
sink(1);
return max;
}
//使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
private void sink(int k) {
//循环对比k结点和其左子结点2k以及右子结点2k+1处中的较大值的元素大小,如果当前结点小,则需要交换位置
while (2 * k heap = new Heap(20);
heap.insert("A");
heap.insert("B");
heap.insert("C");
heap.insert("D");
heap.insert("E");
heap.insert("F");
heap.insert("G");
String del;
//循环删除
while ((del = heap.delMax()) != null) {
System.out.print(del + ",");
}
}
}
④哈希表
❶基础知识
哈希表(Hash table),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做哈希表。
常见的三种哈希结构
- 数组
int[] hashTable = new int[26]; //存26字母索引
//hashTable[s.charAt(i) - 'a']++; 字母存在则在对应位置加1
- set (集合)
Set set = new HashSet();
//set.add(num) 插入元素
//set.contains(num) 查找键是否存在
- map(映射)
Map map = new HashMap();
//map.put(key,value) 插入元素
//map.getOrDefault(ch, 0); 查询map是否存在ch,不存在设置默认值0
//map.values() 返回所有value
//map.containsKey(key) 查找键是否存在
//map.isEmpty() 判断是否为空
//map.get() 根据键获取值
//map.remove() 根据键删除映射关系
⑤字符串
双指针:344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
class Solution {
public void reverseString(char[] s) {
int left = 0, right = s.length - 1;
while(left
双指针: 541. 反转字符串 II
给定一个字符串 s
和一个整数 k
,从字符串开头算起,每计数至 2k
个字符,就反转这 2k
字符中的前 k
个字符。
- 如果剩余字符少于
k
个,则将剩余字符全部反转。 - 如果剩余字符小于
2k
但大于或等于k
个,则反转前k
个字符,其余字符保持原样。
示例 1:
输入:s = "abcdefg", k = 2
输出:"bacdfeg"
示例 2:
输入:s = "abcd", k = 2
输出:"bacd"
题意:每隔2k个反转前k个,尾数不够k个时候全部反转
class Solution {
public String reverseStr(String s, int k) {
int n = s.length();
char[] arr = s.toCharArray();
// 1. 每隔 2k 个字符的前 k 个字符进行反转
for (int i = 0; i
双指针:345. 反转字符串中的元音字母
给你一个字符串 s
,仅反转字符串中的所有元音字母,并返回结果字符串。
元音字母包括 'a'
、'e'
、'i'
、'o'
、'u'
,且可能以大小写两种形式出现不止一次。
示例 1:
输入:s = "hello"
输出:"holle"
示例 2:
输入:s = "leetcode"
输出:"leotcede"
class Solution {
public String reverseVowels(String s) {
//定义两个哨兵
int l = 0, r = s.length() - 1;
char[] arr = s.toCharArray();
while (l
双指针:1768. 交替合并字符串
给你两个字符串 word1
和 word2
。请你从 word1
开始,通过交替添加字母来合并字符串。如果一个字符串比另一个字符串长,就将多出来的字母追加到合并后字符串的末尾。
返回 合并后的字符串 。
示例 1:
输入:word1 = "abc", word2 = "pqr"
输出:"apbqcr"
解释:字符串合并情况如下所示:
word1: a b c
word2: p q r
合并后: a p b q c r
示例 2:
输入:word1 = "ab", word2 = "pqrs"
输出:"apbqrs"
解释:注意,word2 比 word1 长,"rs" 需要追加到合并后字符串的末尾。
word1: a b
word2: p q r s
合并后: a p b q r s
示例 3:
输入:word1 = "abcd", word2 = "pq"
输出:"apbqcd"
解释:注意,word1 比 word2 长,"cd" 需要追加到合并后字符串的末尾。
word1: a b c d
word2: p q
合并后: a p b q c d
class Solution {
public String mergeAlternately(String word1, String word2) {
StringBuilder sb = new StringBuilder();
int m = word1.length(), n = word2.length();
int i = 0, j = 0;
while(i
⑥双指针
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
class Solution {
public void reverseString(char[] s) {
int left = 0, right = s.length - 1;
while(left
27. 移除元素
给你一个数组 nums
和一个值 val
,你需要原地移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
//双指针
class Solution {
public int removeElement(int[] nums, int val) {
//快慢指针解法
int slow = 0; //慢指针
//快指针,无论与val值是否相同每遍历一次都要移动一位
for(int fast = 0; fast
283. 移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
class Solution {
public void moveZeroes(int[] nums) {
// 去除 nums 中的所有 0
// 返回去除 0 之后的数组长度
int p = removeElement(nums, 0);
// 将 p 之后的所有元素赋值为 0
for (; p
快排思想,用 0
当做这个中间点,把不等于 0
的放到中间点的左边,等于 0
的放到其右边。使用两个指针 i
和 j
,只要 nums[i]!=0
,我们就交换 nums[i]
和 nums[j]
class Solution {
public void moveZeroes(int[] nums) {
if(nums == null) return;
//两个指针i和j
int j = 0;
for(int i = 0; i
⑦二叉树
二叉树基础知识
- 二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个节点最多只能有两棵子树,且有左右之分
- 二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个节点
//Definition for a binary tree node.
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
我们有一棵二叉树:
根
/
左 右
栈是一种 先进后出
的结构,那么入栈顺序必须调整为倒序
- 前序遍历,出栈顺序:根左右; 入栈顺序:右左根
- 中序遍历,出栈顺序:左根右; 入栈顺序:右根左
- 后序遍历,出栈顺序:左右根; 入栈顺序:根右左
❶二叉树遍历
144. 二叉树的前序遍历
先输出父节点,再遍历左子树和右子树
1.递归
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List preorderTraversal(TreeNode root) {
List res = new ArrayList();
dfs(root, res);
return res;
}
public void dfs(TreeNode root, List res){
if(root == null){
return;
}
res.add(root.val);
dfs(root.left, res);
dfs(root.right, res);
}
}
2.迭代
- 弹栈顶入列表
- 如有右,先入右
- 如有左,再入左
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List preorderTraversal(TreeNode root) {
List res = new ArrayList();
if(root == null) {
return res;
}
Deque stack = new LinkedList();// 用栈来实现迭代
stack.push(root);
while(!stack.isEmpty()){
TreeNode tmp = stack.pop();
res.add(tmp.val);
if(tmp.right != null){ //先进右节点,后出
stack.push(tmp.right);
}
if(tmp.left != null){ //后进左节点,先出
stack.push(tmp.left);
}
}
return res;
}
}
94. 二叉树的中序遍历
先遍历左子树,再输出父节点,再遍历右子树
1.递归
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List inorderTraversal(TreeNode root) {
List res = new ArrayList();
dfs(root, res);
return res;
}
public void dfs(TreeNode root, List res){
if(root == null){
return;
}
dfs(root.left, res);
res.add(root.val);
dfs(root.right, res);
}
}
2.迭代
- 根结点不为空,入栈并向左走。整条界依次入栈
- 根结点为空,弹栈顶打印,向右走。
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List inorderTraversal(TreeNode root) {
List res = new ArrayList();
Deque stack = new LinkedList();
while (root != null || !stack.isEmpty()) {
while (root != null) { // 将根和左子树入栈
stack.push(root);
root = root.left;
}
//当前节点为空,说明左边走到头了,从栈中弹出节点并保存
TreeNode tmp = stack.pop();
res.add(tmp.val);
//然后转向右边节点,继续上面整个过程
root = tmp.right;
}
return res;
}
}
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List inorderTraversal(TreeNode root) {
List res = new ArrayList();
Deque stack = new LinkedList();
while (root != null || !stack.isEmpty()) {
if (root != null) {
stack.push(root);
root = root.left;
} else {
TreeNode tmp = stack.pop();
res.add(tmp.val);
root = tmp.right;
}
}
return res;
}
}
145. 二叉树的后序遍历
先遍历左子树,再遍历右子树,最后输出父节点
1.递归
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List postorderTraversal(TreeNode root) {
List res = new ArrayList();
dfs(root, res);
return res;
}
public void dfs(TreeNode root ,List res){
if(root == null){
return;
}
dfs(root.left, res);
dfs(root.right, res);
res.add(root.val);
}
}
2.迭代
- 弹栈顶输出
- 如有左,压入左
- 如有右,压入右
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List postorderTraversal(TreeNode root) {
List res = new ArrayList();
if (root == null) {
return res;
}
Deque stack = new LinkedList();
TreeNode prev = null;
while (!stack.isEmpty() || root != null) {
while (root != null) { // 将左子树全部入栈
stack.push(root);
root = root.left;
}
root = stack.pop(); // 拿取栈顶节点
if (root.right == null || root.right == prev) {
res.add(root.val);
prev = root;
root = null;
} else {
// 重新把根节点入栈,处理完右子树还要回来处理根节点
stack.push(root);
// 当前节点为右子树
root = root.right;
}
}
return res;
}
}
class Solution {
public List postorderTraversal(TreeNode root) {
List res = new ArrayList();
Deque stack = new LinkedList();
if(root == null){
return res;
}
// 如果当前处理节点不为空或者栈中有数据则继续处理
stack.push(root);
while (!stack.isEmpty()) {
TreeNode tmp = stack.pop();
res.add(tmp.val);
if(tmp.left != null) stack.push(tmp.left);
if(tmp.right != null) stack.push(tmp.right); //出栈根右左
}
Collections.reverse(res);//反转之后:左右根
return res;
}
}
102. 二叉树的层序遍历
1.递归
/**
*时间:O(n)
*空间:O(h)
**/
class Solution {
public List> levelOrder(TreeNode root) {
List> res = new ArrayList>();
dfs(root, 0, res);
return res;
}
public void dfs(TreeNode root, Integer level, List> res) {
if (root == null) return;
if (res.size() item = new ArrayList();
res.add(item);
}
//遍历到第几层我们就操作第几层的数据
List list = res.get(level);
list.add(root.val);
//分别遍历左右两个子节点,到下一层了,所以层数要加1
dfs(root.left, level + 1, res);
dfs(root.right, level + 1, res);
}
}
2.迭代
/**
*时间:O(n)
*空间:O(n)
**/
//借助队列
class Solution {
public List> levelOrder(TreeNode root) {
List> res = new ArrayList>();
if (root == null) {
return res;
}
Queue queue = new LinkedList();
//Queue queue = new ArrayDeque();
queue.offer(root);
while (!queue.isEmpty()) {
List level = new ArrayList();
int currentLevelSize = queue.size();
for (int i = 0; i
103. 二叉树的锯齿形层序遍历
给你二叉树的根节点 root
,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
递归
class Solution {
public List> zigzagLevelOrder(TreeNode root) {
List> res = new ArrayList>();
dfs(root, 0, res);
return res;
}
public void dfs(TreeNode root, Integer level, List> res) {
if (root == null) return;
if (res.size() item = new ArrayList();
res.add(item);
}
//遍历到第几层我们就操作第几层的数据
List list = res.get(level);
if (level % 2 == 0){
list.add(root.val); //根节点是第0层,偶数层相当于从左往右遍历,所以要添加到集合的末尾
} else {
list.add(0, root.val); //如果是奇数层相当于从右往左遍历,要把数据添加到集合的开头
}
//分别遍历左右两个子节点,到下一层了,所以层数要加1
dfs(root.left, level + 1, res);
dfs(root.right, level + 1, res);
}
}
迭代
class Solution {
public List> zigzagLevelOrder(TreeNode root) {
List> res = new ArrayList>();
if(root == null){
return res;
}
Queue queue = new LinkedList();
queue.offer(root);
boolean flag = true; // 为 true 时从左开始,false 时从右开始,第一步先从左边开始打印
while(!queue.isEmpty()){
List list = new ArrayList();
int n = queue.size();
for(int i = 0; i
⑧图
图这种数据结构有一些比较特殊的算法,比如二分图判断,有环图无环图的判断,拓扑排序,以及最经典的最小生成树,单源最短路径问题,更难的就是类似网络流这样的问题。
参考:图论基础及遍历算法、二分图判定算法、环检测和拓扑排序、图遍历算法、名流问题、并查集算法计算连通分量、Dijkstra 最短路径算法
图论基础
「图」的两种表示方法,邻接表(链表),邻接矩阵(二维数组)。
邻接矩阵判断连通性迅速,并可以进行矩阵运算解决一些问题,但是如果图比较稀疏的话很耗费空间。
邻接表比较节省空间,但是很多操作的效率上肯定比不过邻接矩阵。
邻接表把每个节点 x
的邻居都存到一个列表里,然后把 x
和这个列表关联起来,这样就可以通过一个节点 x
找到它的所有相邻节点。
邻接矩阵则是一个二维布尔数组,我们权且称为 matrix
,如果节点 x
和 y
是相连的,那么就把 matrix[x][y]
设为 true
(上图中绿色的方格代表 true
)。如果想找节点 x
的邻居,去扫一圈 matrix[x][..]
就行了。
如果用代码的形式来表现,邻接表和邻接矩阵大概长这样:
// 邻接表
// graph[x] 存储 x 的所有邻居节点
List[] graph;
// 邻接矩阵
// matrix[x][y] 记录 x 是否有一条指向 y 的边
boolean[][] matrix;
邻接表建立
//把图转化成邻接表
List[] buildGraph(int x, int[][] edges) {
// 图中共有 x 个节点
List[] graph = new LinkedList[x];
for (int i = 0; i ();
}
// edges = [[1,0],[0,1]]
for (int[] edge : edges) {
// from = 0, to = 1
int from = edge[1], to = edge[0];
// 添加一条从 from 指向 to 的有向边
graph[from].add(to);
}
return graph;
}
图遍历
图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点,而树不会出现这种情况,从某个节点出发必然走到叶子节点,绝不可能回到它自身。
所以,如果图包含环,遍历框架就要一个 visited
数组进行辅助:
// 记录被遍历过的节点
boolean[] visited;
// 记录从起点到当前节点的路径
boolean[] onPath;
/* 图遍历框架 */
void traverse(Graph graph, int s) {
if (visited[s]) return; // 已被遍历
// 经过节点 s,标记为已遍历
visited[s] = true;
// 做选择:标记节点 s 在路径上
onPath[s] = true;
for (int neighbor : graph[s] {
traverse(graph, neighbor);
}
// 撤销选择:节点 s 离开路径
onPath[s] = false;
}
环检测
类比贪吃蛇游戏,visited
记录蛇经过过的格子,而 onPath
仅仅记录蛇身。onPath
用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景。
// 记录一次递归路径中的节点
boolean[] onPath;
// 记录遍历过的节点,防止走回头路
boolean[] visited;
// 记录图中是否有环
boolean hasCycle = false;
void traverse(List[] graph, int s) {
// 出现环
if (onPath[s]) {
hasCycle = true;
}
// 如果已经找到了环,也不用再遍历了
if (visited[s] || hasCycle) {
return;
}
// 前序代码位置
visited[s] = true; // 将当前节点标记为已遍历
onPath[s] = true; // 开始遍历节点 s
for (int neighbor : graph[s]) {
traverse(graph, neighbor);
}
// 后序代码位置
onPath[s] = false; // 节点 s 遍历完成
}
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net