队列
队列是一种特殊的表结构,它只允许在表的前端front进行删除操作,在表的后端rear进行插入操作,和栈一样。队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
Java Queue基本方法
boolean add(E e) //将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。
E remove() //移除并返回队列头部的元素 ,如果队列为空,则抛出一个NoSuchElementException异常
E element() //返回队列头部的元素 ,但是不移除此队列的头,如果队列为空,则抛出一个NoSuchElementException异常。
boolean offer(E e) //添加一个元素并返回true 如果队列已满,则返回false,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。
E peek() //返回队列头部的元素;如果此队列为空,则返回null。
E poll() //移除并返问队列头部的元素,如果此队列为空,则返回 null。
Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。
JZ 09:用两个栈实现队列:
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
分析题目要求要用栈实现,但是栈是先进后出,队列是先进先出的,所以栈无法实现队列的功能,栈底元素,也就是对应的队首元素,没办法直接删除,必须将上方的所有元素出栈。但是双栈就可实现这一功能,将栈A的元素出栈并入到栈B里面,容易想到栈B就是栈A的元素倒序,利用这个倒序的栈就可以轻易删除队首元素。简单来说就是:
- 栈A实现入队功能;
- 栈B实现出队功能
public class demo3 {
Stack<Integer> stackA;
Stack<Integer> stackB;
public demo3(){
stackA = new Stack<Integer>();
stackB = new Stack<Integer>();
}
/**
* 队列尾部插入整数
* @param value
*/
public void appendTail(int value){
stackA.push(value);
}
/**
* 队列头部删除整数
* @return
*/
public int deleteHead(){
if (stackB.isEmpty()){
while (!stackA.isEmpty()){
stackB.push(stackA.pop());
}
}
if (stackB.isEmpty()){
return -1;
}else {
int deleteItem = stackB.pop();
return deleteItem;
}
}
}
树的基本术语:
- 结点的度:结点拥有的子树的数目
- 叶子结点:度为0的结点
- 分支结点:度不为0的结点
- 树的度:树中结点的最大的度
- 层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1
- 树的高度:树中结点的最大层次
- 森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林
二叉树基础知识:
1.二叉树定义:
- 二叉树是每个结点最多有两个子树的树结构。它有五种基本形态:二叉树可以是空集,非空二叉树只有一个根节点;根可以有空的左子树或右子树;或者左、右子树皆为空。
- 满二叉树也是完全二叉树,而完全二叉树一般不是满二叉树,注意二者的区别。完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。
2.二叉树性质:
-
二叉树第i层上的结点数目最多为2^i-1(i>=1)
-
深度为k的二叉树至多有2^k-1个结点(k>=1)
-
包含n个结点的二叉树的高度至少为[log2n]+1,其中[log2n]表示取log2n的整数部分。
-
在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1,也就是说,度为0的节点(叶子节点)总是比度为2的节点多一个。
证明:
因为二叉树中所有结点的度数均不大于2,所以结点总数(记为n)=“0度结点数(n0)” + “1度结点数(n1)” + “2度结点数(n2)”。由此,得到等式一。
(等式一) n=n0+n1+n2
另一方面,0度结点没有孩子,1度结点有一个孩子,2度结点有两个孩子,故二叉树中孩子结点总数是:n1+2n2。此外,只有根不是任何结点的孩子。故二叉树中的结点总数又可表示为等式二。
(等式二) n=n1+2n2+1
由(等式一)和(等式二)计算得到:n0=n2+1。原命题得证!
JZ 32 从上到下打印二叉树
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
3
/ \
9 20
/ \
15 7
[
[3],
[9,20],
[15,7]
]
每一层打印一行有点难,可以先考虑怎么把它们打印到一行里面,也就是:
[3,9,20,15,7]
其实题目要求的二叉树的从上到下的打印也就是二叉树的广度优先搜索(BFS),而BFS通常借助队列的先入先出特性来实现:
我们上面说过:
Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。
流程思路:
- 特例处理: 当树的根节点为空,则直接返回空列表
[]
; - 初始化: 打印结果列表
res = []
,包含根节点的队列queue = [root]
; - BFS 循环: 当队列
queue
为空时跳出;- 出队: 队首元素出队,记为 node;
- 打印: 将 node.val 添加至列表 tmp 尾部;
- 添加子节点: 若 node 的左(右)子节点不为空,则将左(右)子节点加入队列 queue ;
- 返回值: 返回打印结果列表
res
即可。
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x){val = x;}
}
public int[] levelOrder(TreeNode root){
//空树则返回空数组
if (root==null){
return new int[0];
}
Queue<TreeNode> queue = new LinkedList<>();
ArrayList<Integer> temp = new ArrayList<>();
queue.offer(root);//根节点先入队
while (!queue.isEmpty()){
TreeNode node = queue.poll();//队首元素出队
temp.add(node.val);//node.val添加到temp的尾部
//添加子节点
if (node.left!=null){
queue.offer(node.left);//左子节点入队
}
if (node.right!=null){
queue.offer(node.right);//右子节点入队
}
}
//将ArrayList转为int数组并返回
int[] res = new int[temp.size()];
for (int i=0; i<res.length; i++){
res[i] = temp.get(i);
}
return res;
}
套到上面那个例子里面就是:
先初始化queue和res,此时queue[3],res[],,再出队,打印,添加左右节点,此时queue[9,2],res[3],下一次的出队出9,res打印9,而左右节点为空所以跳过,下一次的出队出2,res打印2,添加左右节点1,7,此时queue[1,7],res[3,9,2],下一次的出队出1,res打印1,而左右节点为空所以跳过;下一次的出队出7,res打印7,而左右节点为空所以跳过
over!!!
看到有大佬用Deque实现:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int[] levelOrder(TreeNode root) {
Deque<TreeNode> deque = new ArrayDeque<>(); // 栈
if(root != null){
deque.add(root);
}
List<Integer> list = new ArrayList<>();
// 层次遍历
while(!deque.isEmpty()){
TreeNode node = deque.remove(); // 弹出并且出栈
list.add(node.val); // 加入list集合
if(node.left != null){
deque.add(node.left);
}
if(node.right != null){
deque.add(node.right);
}
}
int len = list.size();
int[] arr = new int[len];
for(int i=0; i<len; i++){
arr[i] = list.get(i);
}
return arr;
}
}
接下来回到怎么分层打印的问题:
public List<List<Integer>> levelOrder(TreeNode root){
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
if (root!=null){
queue.offer(root);
}
while (!queue.isEmpty()){
//新建一个临时列表temp,用于存储当前层打印结果
List<Integer> temp = new ArrayList<>();
//注意与上面第一个代码的区别
//打印次数不同了
for(int i=queue.size();i>0;i--){
TreeNode node = queue.poll();//队首元素出队
temp.add(node.val);//node.val添加到temp的尾部
//添加子节点
if (node.left!=null){
queue.offer(node.left);
}
if (node.right!=null){
queue.offer(node.right);
}
}
//将当前层结果temp添加入res
res.add(temp);
}
return res;
}
我看了下这是所有解法里面最秀的一种,每层打印到一行的时候,利用当前层的打印循环(循环次数为当前层节点数,也就是队列queue长度),这个len(queue)我真是想破头也想不出来,给大佬献上膝盖Orz!!!
这个题还是挺有意思的,可以好好儿理解一下!!!
面试题32 - II. 从上到下打印二叉树 II(层序遍历 BFS,清晰图解) - 从上到下打印二叉树 II - 力扣(LeetCode) (leetcode-cn.com)
看了这几道题脑壳疼,来几道开胃小菜放松一下吧:
开胃小菜–力扣1(两数之和)
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
这题用暴力破解法很容易就写出来了,两层for循环O(N^2):
class Solution {
public int[] twoSum(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[0];
}
}
还可以利用哈希表来实现:
Java HashMap | 菜鸟教程 (runoob.com)
创建一个哈希表,对于每一个 x
,我们首先查询哈希表中是否存在 target - x
,然后将 x
插入到哈希表中,即可保证不会让 x
和自己匹配。
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (hashtable.containsKey(target - nums[i])) {
return new int[]{hashtable.get(target - nums[i]), i};
}
hashtable.put(nums[i], i);
}
return new int[0];
}
复杂度分析
- 时间复杂度:O(N),其中 N是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。
- 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
今日知识小点
//使用foreach循环输出数组中的元素
for(int num:nums){
System.out.print(num+" ");
}
//等价于
for (int i =0; i <= nums.length; i++) {
.....
}
Array.sort();//对数组元素排序
ListArray继承并实现了List。
所以List不能被构造,但可以向上面那样为List创建一个引用,而ListArray就可以被构造。
List list; //正确 list=null;
List list=new List(); // 是错误的用法
List list = new ArrayList();//这句创建了一个ArrayList的对象后把上溯到了List。此时它是一个List对象了,有些ArrayList有但是List没有的属性和方法,它就不能再用了。
而ArrayList list=new ArrayList();//创建一对象则保留了ArrayList的所有属性。
int data[] = new int[3]; /*开辟了一个长度为3的数组*/
return new int[0];//返回空数组
return new int[]{i, j};//这种写法有点忘记了
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();//键值对的形式
今日推歌
—《那年》任然
想去稻花香的童年 捡起被遗忘的相片
曾经弹过的木吉他早就断了弦
还记得年少时我最爱荡秋千
一个人骑车去海边 等待流星许下心愿
蝴蝶风筝飞得很远 随着天空消失不见
曾经听过的寓言都被岁月搁浅
和你遥望过的月光都没有变
让四季都变成夏天 放晴每一天
无人不爱任然!!!!!