学习思路如下:
八、算法与数据结构—题目
❤1、哈希
1、hashset存的数是有序的吗?
2、Object作为HashMap的key的话,对Object有什么要求吗?
3、一致性哈希算法。
4、什么是hashmap?
5、Java中的HashMap的工作原理是什么?
6、hashCode()和equals()方法的重要性体现在什么地方?
❤2、树
1、说一下B+树和B-树?
2、怎么求一个二叉树的深度?手撕代码?
3、算法题:二叉树层序遍历,进一步提问:要求每层打印出一个换行符
4、二叉树任意两个节点之间路径的最大长度
5、如何实现二叉树的深度?
6、如何打印二叉树每层的节点?
7、TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
❤3、遍历
1、编程题:写一个函数,找到一个文件夹下所有文件,包括子文件夹
2、二叉树,Z字型遍历
❤4、链表
1、反转单链表
2、随机链表的复制
3、链表-奇数位升序偶数位降序-让链表变成升序
4、bucket如果用链表存储,它的缺点是什么?
5、如何判断链表检测环
❤5、数组
1、寻找一数组中前K个最大的数
2、求一个数组中连续子向量的最大和
3、找出数组中和为S的一对组合,找出一组就行
4、一个数组,除一个元素外其它都是两两相等,求那个元素?
5、算法题:将一个二维数组顺时针旋转90度,说一下思路。
❤6、排序
1、排序算法知道哪些,时间复杂度是多少,解释一下快排?
2、如何得到一个数据流中的中位数?
3、堆排序的原理是什么?
4、归并排序的原理是什么?
5、排序都有哪几种方法?请列举出来。
6、如何用java写一个冒泡排序?
❤7、堆与栈
1、堆与栈的不同是什么?
2、heap和stack有什么区别。
3、解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法。
❤8、队列
1、什么是Java优先级队列(Priority Queue)?
答案部分:1)哈希
1、hashset存的数是有序的吗?
答:Hashset 是无序的。
hashset储存的是唯一的对象,没有下标,只能通过增强for,或者迭代器进行遍历。当出现相同的对象,会合并,只储存一个空间。
Iterator<news> it=set.iterator();
---------------------------------
while(it.hasNext()){
News n=it.next();
}
---------------------------------
for(News n:set){
printf(n);
}
2、Object作为HashMap的key的话,对Object有什么要求吗?
答:Hashmap不允许有重复的key,所以要重写它的hashcode和equal方法,以便确认key是否重复,即要求 Object 中 hashcode 不能变。
3、一致性哈希算法
答:详细参考—https://www.cnblogs.com/williamjie/p/9477852.html
- 例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:
根据一致性哈希算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。
下面分析一致性哈希算法的容错性和可扩展性。现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。
4、什么是hashmap?
答:HashMap是用哈希表(直接一点可以说数组加单链表)+红黑树实现的map类。
- HashMap的存储结构:
上图便是HashMap的存储结构,HashMap的这种特殊存储结构在获取指定元素前需要把key经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素,这使得 HashMap 的查找效率极高。(说白了HashMap就是用了拉链法的哈希表,也有叫桶数组的)
HashMap的特点: - 底层实现是链表数组,JDK 8后又加了红黑树
- 实现了Map全部的方法
- key用Set存放,所以想做到 key 不允许重复,key对应的类(一般是String)需要重写 hashCode和equals方法
- 允许空键和空值(但空键只有一个,且放在第一位,知道就行)
- 元素是无序的,而且顺序会不定时改变(每次扩容后,都会重新哈希,也就是key通过哈希函数计算后会得出与之前不同的哈希值,这就导致哈希表里的元素是没有顺序,会随时变化的,这是因为哈希函数与桶数组容量有关,每次结点到了临界值后,就会自动扩容,扩容后桶数组容量都会乘二,而key不变,那么哈希值一定会变)
- 插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置)
- 遍历整个 Map 需要的时间与数组的长度成正比(因此初始化时 HashMap 的容量不宜太大)
- 两个关键因子:初始容量、加载因子
- HashMap不是同步,HashTable是同步的,但HashTable已经弃用,如果需要线程安全,可以用synchronizedMap,例如:Map m = Collections.synchronizedMap(new HashMap(…));
5、Java中的HashMap的工作原理是什么?
答:Java中的HashMap是以键值对(key-value)的形式存储元素的。
HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。HashMap的一些重要的特性是它的容量(capacity),负载因子(load factor)和扩容极限(threshold resizing)。
- 有以下三方面:
1)hashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。
2)hashTable同步的,而HashMap是非同步的,效率上逼hashTable要高。
3)hashMap允许空键值,而hashTable不允许。
6、hashCode()和equals()方法的重要性体现在什么地方?
答:
Java中的HashMap使用hashCode()和equals()方法来确定键值对的索引,当根据键获取值的时候也会用到这两个方法。如果没有正确的实现这两个方法,两个不同的键可能会有相同的hash值,因此可能会被集合认为是相等的。而且,这两个方法也用来发现重复元素,所以这两个方法的实现对HashMap的精确性和正确性是至关重要的。
答案部分:2)树
1、说一下B+树和B-树?
答:
两者区别:
b+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”; b+树查询必须查找到叶子节点,b- 树只要匹配到即可不用管元素位置,因此 b+树查找更稳定 (并不慢); 对于范围查找来说,b+树只需遍历叶子节点链表即可,b- 树却需要重复地中序遍历;
1)什么是B-树:
B树也叫B-树,是一棵多路平衡查找树。描述B树使用阶树m,表示了他最多有多少孩子结点,一个二叉树最多有两个孩子结点,所以当m=2时,也就是二叉搜索树。
如图所示,假设B树的阶数为m:
1)每个结点最多有m-1个关键字。
2)根结点最少可以只有1个关键字。
3)非根结点至少有Math.ceil(m/2)-1个关键字。
4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
5)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。
2.什么是B+树:
一种定义方式是关键字个数和孩子结点个数相同。这里我们采取维基百科上所定义的方式,即关键字个数比孩子结点个数小1,这种方式是和B树基本等价的。
如图一颗阶数为4的B+树为例:
1)B+树包含2种类型的结点:内部结点(也称索引结点)和叶子结点。根结点本身即可以是内部结点,也可以是叶子结点。根结点的关键字个数最少可以只有1个。
2)B+树与B树最大的不同是内部结点不保存数据,只用于索引,所有数据(或者说记录)都保存在叶子结点中。
3) m阶B+树表示了内部结点最多有m-1个关键字(或者说内部结点最多有m个子树),阶数m同时限制了叶子结点最多存储m-1个记录。
4)内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
5)叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。
2、怎么求一个二叉树的深度?手撕代码?
答:
- 1)后序遍历,最长栈长即为树的深度
- 2)递归,最后比较即可
- 3)遍历求层,层次即为深度
思路: 二叉树的最大深度等于左右子树的最大深度中的较大者+1。
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
3、算法题:二叉树层序遍历,进一步提问:要求每层打印出一个换行符
答:遍历一棵二叉树常用的有四种方法,前序(PreOrder)、中序(InOrder)、后序(PastOrder)和层序(LevelOrder)。
-
如下图为一棵二叉树:
前序遍历:ABDECFG
中序遍历:DBEAFCG
后序遍历:DEBFGCA
层序遍历:ABCDEFG。 -
实现二叉树层序遍历:
-
思路:
从最上面一层,一层一层往下,直到遍历完所有的子节点,通过这一点,队列操作比较符合,首先用front指向根节点a,将a入队,然后a出队,遍历a,输出为:A,将a的左右孩子依次入队,也就是bc,b出队,遍历b,输出为:AB,将b的左右孩子入队,de入队,c出队,遍历c,输出为:ABC,将c的左右孩子fg入队,循环遍历直到遍历结束。
public void LevelOrder(TreeNode root) {
Queue<Node> queue = new LinkedList<Node>();
TreeNode front = new TreeNode();
if(root == null) return ;
queue.push(root)
while(!queue.isEmpty()) {
front = root;
System.out,println(front.value);
queue.pop();
if(root.left != null) {
queue.push(root.left);
}
if(root.right != null) {
queue.push(root.right);
}
System.out,println(' ');
}
}
4、二叉树任意两个节点之间路径的最大长度
答:
- 树的高度:树根的高度就是树的高度。
- 树的深度:最深的叶结点的深度就是树的深度。
- 对于树中相同深度的每个结点来说,它们的高度不一定相同,这取决于每个结点下面的叶结点的深度。
- 思路:
要计算一个树任意两个节点的最大距离,从根节点出发,无非就两种情况。
(1)一种是最长路径经过根节点,
(2)另一种是最长路径不经过根节点。
1)对于情况1,左子树的高度加上右子树的高度即为整棵树的最大路径。
2)对于情况2,既然不经过根节点,那就可以再次分为两种情况,
2.1)整棵树的最大路径是左子树的最大路径,
2.2)整棵树的最大路径是右子树的最大路径。
而下面的两种情况又可以归结为最初始的求树的最大路径算法,各分为过根节点和不过根节点的两种情况。如此通过递归算法,最终可以求得整棵树的最大路径。
int maxDist(Tree root) {
//如果树是空的,则返回 0
if(root == NULL)
return 0;
if(root->left != NULL) {
root->lm = maxDist(root->left) + 1;
}
if(root->right != NULL) root->rm = maxDist(root->right) + 1;
//如果以该节点为根的子树中有最大的距离,那就更新最大距离
int sum = root->rm + root->lm;
if(sum > max) {
max = sum;
}
return root->rm > root->lm ? root->rm : root->lm;
}
5、如何实现二叉树的深度?
答:
- 思路:
1)递归实现
为了求树的深度,可以先求其左子树的深度和右子树的深度,可以用递归实现,递归的出口就是节点为空。返回值为0;
2)非递归实现
利用层次遍历的算法,设置变量level记录当前节点所在的层数,设置变量last指向当前层的最后一个节点,当处理完当前层的最后一个节点,让level指向+1操作。设置变量cur记录当前层已经访问的节点的个数,当cur等于last时,表示该层访问结束。
层次遍历在求树的宽度、输出某一层节点,某一层节点个数,每一层节点个数都可以采取类似的算法。
树的宽度:
在树的深度算法基础上,加一个记录访问过的层节点个数最多的变量max,在访问每层前max与last比较,如果max比较大,max不变,如果max小于last,把last赋值给max;
代码:
import java.util.LinkedList;
public class Deep
{
//递归实现1
public int findDeep(BiTree root)
{
int deep = 0;
if(root != null)
{
int lchilddeep = findDeep(root.left);
int rchilddeep = findDeep(root.right);
deep = lchilddeep > rchilddeep ? lchilddeep + 1 : rchilddeep + 1;
}
return deep;
}
//递归实现2
public int findDeep1(BiTree root)
{
if(root == null)
{
return 0;
}
else
{
int lchilddeep = findDeep1(root.left);//求左子树的深度
int rchilddeep = findDeep1(root.right);//求右子树的深度
return lchilddeep > rchilddeep ? lchilddeep + 1 : rchilddeep + 1;//左子树和右子树深度较大的那个加一等于整个树的深度
}
}
//非递归实现
public int findDeep2(BiTree root)
{
if(root == null)
return 0;
BiTree current = null;
LinkedList<BiTree> queue = new LinkedList<BiTree>();
queue.offer(root);
int cur,last;
int level = 0;
while(!queue.isEmpty())
{
cur = 0;//记录本层已经遍历的节点个数
last = queue.size();//当遍历完当前层以后,队列里元素全是下一层的元素,队列的长度是这一层的节点的个数
while(cur < last)//当还没有遍历到本层最后一个节点时循环
{
current = queue.poll();//出队一个元素
cur++;
//把当前节点的左右节点入队(如果存在的话)
if(current.left != null)
{
queue.offer(current.left);
}
if(current.right != null)
{
queue.offer(current.right);
}
}
level++;//每遍历完一层level+1
}
return level;
}
public static void main(String[] args)
{
BiTree root = BiTree.buildTree();
Deep deep = new Deep();
System.out.println(deep.findDeep(root));
System.out.println(deep.findDeep1(root));
System.out.println(deep.findDeep2(root));
}
}
6、如何打印二叉树每层的节点?
答:借助一个队列,先把根节点入队,每打印一个节点的值时,也就是打印队列头的节点时,都会把它的的左右孩子入队,并且把该节点出队。直到队列为空。
public class Solution(TreeNode root) {
Queue<Node> queue = new Queue<Node>();
if(root = null)
return ;
queue.push(root.value);
whlie(!queue.isEmpty()) {
if(root.left != null)
queue.push(root.left.value);
if(root.right != null)
queue.push(root.right.value);
System.out.print(queue.pop() + ' ');
}
}
7、TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
答:参考—https://blog.csdn.net/u010339647/article/details/52006746
- TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。
- TreeMap要求存放的键值对映射键必须实现Comparable接口,根据键对元素进行排序。
- Collections工具类的sort方法有两种重载的形式:
1)第一种要求传入的待排序容器中存放对象必须实现Comparable接口以实现元素比较;
2)第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)
答案部分:3)遍历
1、编程题:写一个函数,找到一个文件夹下所有文件,包括子文件夹
答:
1)获取文件目录:File file = new File(fileName);
2)获取目录下所有子文件和文件夹:File[] files = file.listFiles();
3)通过isFile()函数可以判断是否为文件。
4)通过isDirectory()函数可以判断是否为目录。
package com.anji.allways.business.sales.utils;
import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 用递归统计某文件夹下文件
*/
public class FileCount {
private static AtomicInteger count = new AtomicInteger(0);
public static void readFile(File file) {
File[] fileList = file.listFiles();
if (fileList == null) {
return;
}
for (File f : fileList) {
if (f.isFile()) {
count.incrementAndGet();
System.out.println("文件名" + f.getName());
}
if (f.isDirectory()) {
readFile(f);
}
}
}
public static void main(String[] args) {
//取得目标目录
File file = new File("D:");
//获取目录下子文件及子文件夹
File[] files = file.listFiles();
readfile(files);
/*File file = new File("D:");
readFile(file);*/
System.out.printf("文件总量:" + count);
}
public static void readfile(File[] files) {
if (files == null) {// 如果目录为空,直接退出
return;
}
for (File f : files) {
//如果是文件,直接输出名字
if (f.isFile()) {
count.incrementAndGet();
System.out.println(f.getName());
}
//如果是文件夹,递归调用
else if (f.isDirectory()) {
readfile(f.listFiles());
}
}
}
}
2、二叉树,Z字型遍历?
答:
- 题目: 按照z字形层次遍历二叉树(以根节点所在层为第1层,则第二层的变量从右边节点开始直到最左边节点,第三层遍历则是从最左边开始到最右边)
- 思路: z字形层次遍历是对层次遍历加上了一个限制条件(即相邻层,从左到右的遍历顺序相反),因此还是可以采用队列来实现,只不过节点接入队列时需要考虑加入的顺序
- 代码: 对节点之间的顺序进行维护
public class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<List<Integer>>();
if(root == null) {
return result;
}
LinkedList<TreeNode> queue = new LinkedList<>();
int depth = 0;
queue.offer(root);
while(!queue.isEmpty()) {
int size = queue.size();
List<Integer> tmp = new ArrayList<>();
for(int i = 0; i < size; i++) {
TreeNode node = null;
//因为是走z字形,所有相邻两层的节点处理是相反的
if(depth%2 == 0) {
node = queue.pollLast();//获取链表最后一个节点
if(node.left != null) {
queue.offerFirst(node.left);
}
if(node.right != null) {
queue.offerFirst(node.right);
}
} else {
node = queue.poll();//获取链表第一个节点
if(node.right != null) {
queue.offer(node.right);
}
if(node.left != null) {
queue.offer(node.left);
}
}
tmp.add(node.val);
}
depth++;
result.add(tmp);
}
return result;
}
}
答案部分:4)链表
1、反转单链表
答:
- (1)结构—单链表node的数据结构定义如下:
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
- (2)思路:
方法1: 将单链表储存为数组,然后按照数组的索引逆序进行反转。
方法2: 使用3个指针遍历单链表,逐个链接点进行反转。
方法3: 从第2个节点到第N个节点,依次逐节点插入到第1个节点(head节点)之后,最后将第一个节点挪到新表的表尾。
方法4: 递归(相信我们都熟悉的一点是,对于树的大部分问题,基本可以考虑用递归来解决。但是我们不太熟悉的一点是,对于单链表的一些问题,也可以使用递归。可以认为单链表是一颗永远只有左(右)子树的树,因此可以考虑用递归来解决。或者说,因为单链表本身的结构也有自相似的特点,所以可以考虑用递归来解决)
2、随机链表的复制
答:实现对一个带有随机节点的链表的拷贝。
- (方法一):准备一个辅助的映射map:
第一次遍历链表,将当前链表的节点和当前节点的拷贝,分别作为key和value存放在map中,
第二次遍历链表,将拷贝的节点,按照原来链表的次序连接起来。
public static Node copyListWithRand1(Node head) {
HashMap<Node, Node> map = new HashMap<Node, Node>();
Node cur = head;
while (cur != null) {
map.put(cur, new Node(cur.data));
cur = cur.next;
}
cur = head;
while (cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
return map.get(head); // map.get(null) 不会报错,会直接放回一个null
}
- (方法二):
- 1)遍历一下链表,拷贝当前的节点(cur)为copy,让当前节点的next指针指向copy,遍历完整个数组之后,形成一个大链表,
- 2)再次遍历链表,一次移动两个节点,并接收住,one ,two,根据one找到one的rand节点,该节点的下一个节点,就是two的rand指针应该指向的节点,
- 3)遍历完成之后,在将整个链表分离成为两个节点。
public static Node copyListWithRand1(Node head) {
// 拷贝数组,形成一个大链表
while(cur != null){
next = cur.next;
cur.next = new Node(cur.data);
cur.next.next = next;
cur = next;
}
// copy the rand
cur = head;
Node curCopy = null;
while(cur != null){
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand != null ? cur.rand.next : null;
cur = next;
}
// split the list
while(cur != null){
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res;
}
3、链表-奇数位升序偶数位降序-让链表变成升序
答:一个链表,奇数位升序偶数位降序,让链表变成升序的。
比如:1 8 3 6 5 4 7 2 9,最后输出1 2 3 4 5 6 7 8 9。
- 思路:
- 这道题可以分成三步:
1)首先,根据奇数位和偶数位拆分成两个链表。
2)然后,对偶数链表进行反转。
3)最后,将两个有序链表进行合并。 - 代码参考—https://blog.csdn.net/weixin_30734435/article/details/99319404
4、bucket如果用链表存储,它的缺点是什么?
答:
- ①查找速度慢,因为查找时,需要循环链表访问
- ②如果进行频繁插入和删除操作,会导致速度很慢。
5、如何判断链表检测环?
答:参考—https://blog.csdn.net/u010983881/article/details/78896293
方法一: 穷举遍历
方法二: 哈希表缓存
方法三: 快慢指针
方法四: Set集合大小变化
答案部分:5)数组
1、寻找一数组中前K个最大的数
答:
算法一: 排序,时间复杂度O(nlogn),空间复杂度O(1)
算法二: 前面k个数都比后面的数的最大值要大,则前面k个数就是最大的k个,时间复杂度O(k*(n-k)),空间复杂度O(1)
算法三: 构建大顶堆,然后调整k次,得到最大的k个数。时间复杂度(k+1)O(nlogn),空间复杂度O(1)
2、求一个数组中连续子向量的最大和
答:参考—https://blog.csdn.net/u014259820/article/details/79628758
1)暴力遍历。从i=0开始到a.length-1往后加,遍历所有的子数组,然后比较每一个子数组的和。时间复杂度O(n^2),空间复杂度O(1)。
2)动态规划。状态方程:max(dp[i]) = getMax(max(dp[i-1]) + a[i], a[i])。我们从头开始遍历数组,遍历到a[i]时,最大和可能是max(dp[i-1])+a[i],也可能是a[i]。时间复杂度O(n),空间复杂度O(n)。
3)非动态规划的方法。我们从头开始累加,初始sum=a[0],临时变量temp=a[0]。从i=1开始,temp = temp+a[i],如果temp小于0,并且发现前面加过的数小于sum,那么舍弃前面的累加值,从i+1开始。
3、找出数组中和为S的一对组合,找出一组就行
答:递归搜索。
public static void fun(int a[],String b, int length, int i, int s)
{
String temp = b;
if(i>=length)
return;
//输出
if(s==0)
System.out.println(temp);
//不取a[i]
fun(a,temp, length, i+1, s);
//取a[i]
temp=temp+a[i];
fun(a,temp, length, i+1, s-a[i]);
}
4、一个数组,除一个元素外其它都是两两相等,求那个元素?
答:
- 方法1: 放集合里去重,求和乘2.再减去原来数组所有元素求和。
- 方法2: 快排排序之后再顺序查,是nlogn + n 的复杂度
- 方法3: 两个for循环,拿第一个数组里面的每一个,和第二个数组里的每一个元素相比较
- 方法4: 将所有元素异或即可,《剑指offer》中的题目简单版
// 异或运算满足交换律,结合律,相同数异或为0,结果就是 0 xor x = x
public static int find1From2(int[] a){
int len = a.length, res = 0;
for(int i = 0; i < len; i++){
res= res ^ a[i];
}
return res;
}
5、算法题:将一个二维数组顺时针旋转90度,说一下思路。
答:规律法—https://blog.csdn.net/qq_36513313/article/details/82184041
答案部分:6)排序
1、排序算法知道哪些,时间复杂度是多少,解释一下快排
答:参考—https://blog.csdn.net/shihuboke/article/details/79387523
2、如何得到一个数据流中的中位数?
答:
-
分析:
使用AVL二叉平衡树的方法和使用最大堆最小堆的方法是总的时间复杂度最优的。但是AVL二叉平衡树没有现成的数据结构的实现,因此可以考虑java集合中的PriorityQueue优先队列(也就是堆,默认为小根堆)来实现比较高校的中位数查找。 -
思路:
将数据序列从中间分为两个部分,左边部分使用大根堆表示,右边部分使用小根堆存储。每遍历一个数据,计数器count加1,当count是偶数时,将数据插入小根堆;当count是奇数时,将数据插入大根堆。当所有数据遍历插入完成后(时间复杂度为O(logn)),如果count最后为偶数,则中位数为大根堆堆顶元素和小根堆堆顶元素和的一半;如果count最后为奇数,则中位数为小根堆堆顶元素。 -
代码:
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
private int count = 0; // 数据流中的数据个数
// 优先队列集合实现了堆,默认实现的小根堆
private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// 定义大根堆,更改比较方式
private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(15, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1; // o1 - o2 则是小根堆
}
});
public void Insert(Integer num) {
if ((count & 1) == 0) {
// 当数据总数为偶数时,新加入的元素,应当进入小根堆
// (注意不是直接进入小根堆,而是经大根堆筛选后取大根堆中最大元素进入小根堆)
// 1.新加入的元素先入到大根堆,由大根堆筛选出堆中最大的元素
maxHeap.offer(num);
int filteredMaxNum = maxHeap.poll();
// 2.筛选后的【大根堆中的最大元素】进入小根堆
minHeap.offer(filteredMaxNum);
} else {
// 当数据总数为奇数时,新加入的元素,应当进入大根堆
// (注意不是直接进入大根堆,而是经小根堆筛选后取小根堆中最大元素进入大根堆)
// 1.新加入的元素先入到小根堆,由小根堆筛选出堆中最小的元素
minHeap.offer(num);
int filteredMinNum = minHeap.poll();
// 2.筛选后的【小根堆中的最小元素】进入小根堆
maxHeap.offer(filteredMinNum);
}
count++;
}
public Double GetMedian() {
// 数目为偶数时,中位数为小根堆堆顶元素与大根堆堆顶元素和的一半
if ((count & 1) == 0) {
return new Double((minHeap.peek() + maxHeap.peek())) / 2;
} else {
return new Double(minHeap.peek());
}
}
}
3、堆排序的原理是什么?
答:参考—https://blog.csdn.net/collonn/article/details/19039931
堆排序实际上是利用堆的性质来进行排序的,
要知道堆排序的原理我们首先一定要知道什么是堆。
- 堆的定义:
堆实际上是一棵完全二叉树。 - 堆满足两个性质:
1、堆的每一个父节点都大于(或小于)其子节点;
2、堆的每个左子树和右子树也是一个堆。 - 堆的分类:
堆分为两类:
1、最大堆(大顶堆):堆的每个父节点都大于其孩子节点;
2、最小堆(小顶堆):堆的每个父节点都小于其孩子节点;
- 堆的存储:
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如下图所示:
4、归并排序的原理是什么?
答:参考—https://www.cnblogs.com/chengxiao/p/6194356.html
- 基本思想:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。
分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
- 合并相邻有序子序列:
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
- 代码实现:
package sortdemo;
import java.util.Arrays;
/**
* Created by chengxiao on 2016/12/8.
*/
public class MergeSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
}
执行结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]
- 总结:
- 归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
5、排序都有哪几种方法?请列举出来。
答:插入排序,希尔排序,冒泡排序,快速排序,选择排序,堆排序,归并排序,基数排序,计数排序,桶排序。
6、如何用java写一个冒泡排序?
答:冒泡排序经典的排序算法
public class TestMaoPao {
public static void sortBub(int [] arr) {
//总的交换次数
int count =0;
//遍历数组的趟数
for( int i =0; i< arr. length-1; i++) {
for( int j =0; j< arr. length- i-1; j++) {
//比较前一个元素与后一个元素的大小,将每趟中最大的元素放在末尾,
// 末尾的元素再下一趟 不用再比较,所以 是 j< arr. length- i-1
//交换前后两个元素的位置
if(arr[ j]> arr[ j+1]) {
int temp = arr[ j];
arr[ j]= arr[ j+1];
arr[ j+1]= temp;
count++;
}
}
}
System. out.println( "交换次数"+count );
}
public static void main(String[] args) {
int [] arr = {2,23,1,34,23,45,12};
sortBub(arr);
for( int i=0; i< arr. length; i++) {
System. out.print( arr[ i]+ " ");
}
}
}
答案部分:7)堆与栈
1、堆与栈的不同是什么?
答:堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。
(1)堆:
- ①堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
1)堆中某个节点的值总是不大于或不小于其父节点的值;
2)堆总是一棵完全二叉树;
3)将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。(常见的堆有二叉堆、斐波那契堆等); - ②堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。
- ③堆是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程。
- ④堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。
(2)栈:
- ①栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。
- ②栈就是一个桶,后放进去的先拿出来,它下面本来有的东西要等它出来之后才能出来(先进后出)
- ③栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的Stack的大小。
- ④堆栈本身就是栈,只是换了个抽象的名字。
(3)区别:
-
1.堆栈空间分配
①栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
②堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。 -
2.堆栈缓存方式
①栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。
②堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。 -
3.堆栈数据结构区别
①堆(数据结构):堆可以被看成是一棵树,如:堆排序。
②栈(数据结构):一种先进后出的数据结构。
2、heap和stack有什么区别?
答:java 的内存分为两类,一类是栈内存,一类是堆内存。
- 1)栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间,用于存储这个方法内部的局部变量,当这个方法
结束时,分配给这个方法的栈会释放,这个栈中的变量也将随之释放。 - 2)堆内存是与栈作用不同的,一般用于存放不放在当前方法栈中的那些数据,例如,使用 new创建的对象都放在堆里,所以,它不会随方法的结束而消失。 方法中的局部变量使用 final修饰后,放在堆中,而不是栈中。
区别:
- 1)heap是堆,stack是栈。
- 2)stack的空间由操作系统自动分配和释放,heap的空间是手动申请和释放的,heap常用new关键字来分配。
- 3)stack空间有限,heap的空间是很大的自由区。
在Java中,
若只是声明一个对象,则先在栈内存中为其分配地址空间,
若再new一下,实例化它,则在堆内存中为其分配地址。 - 4)举例:
数据类型 变量名;这样定义的东西在栈区。
如:Object a = null; 只在栈内存中分配空间
new 数据类型(); 或者malloc(长度); 这样定义的东西就在堆区
如:Object b = new Object(); 则在堆内存中分配空间
3、解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法。
答:Java的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
-
堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身.
3.一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。 -
栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
4.由编译器自动分配释放 ,存放函数的参数值,局部变量的值等. -
静态区/方法区:
1.方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
3.—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 -
代码实例:
AppMain.java
public class AppMain //运行时, jvm 把appmain的信息都放入方法区
{
public static void main(String[] args) //main 方法本身放入方法区。
{
Sample test1 = new Sample( " 测试1 " ); //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面
Sample test2 = new Sample( " 测试2 " );
test1.printName();
test2.printName();
}
}
Sample.java
public class Sample //运行时, jvm 把appmain的信息都放入方法区
{
/** 范例名称 */
private name; //new Sample实例后, name 引用放入栈区里, name 对象放入堆里
/** 构造方法 */
public Sample(String name)
{
this .name = name;
}
/** 输出 */
public void printName() //print方法本身放入 方法区里。
{
System.out.println(name);
}
}
- 代码的执行过程:
系统收到了我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,然后把Appmain类的类信息存放到运行时数据区的方法区中。这一过程称为AppMain类的加载过程。
接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令。这个main()方法的第一条语句就是:
Sample test1=new Sample(“测试1”);
语句很简单啦,就是让java虚拟机创建一个Sample实例,并且呢,使引用变量test1引用这个实例。貌似小case一桩哦,就让我们来跟踪一下Java虚拟机,看看它究竟是怎么来执行这个任务的:
1)Java虚拟机一看,不就是建立一个Sample实例吗,简单,于是就直奔方法区而去,先找到Sample类的类型信息再说。结果呢,嘿嘿,没找到@@,这会儿的方法区里还没有Sample类呢。可Java虚拟机也不是一根筋的笨蛋,于是,它发扬“自己动手,丰衣足食”的作风,立马加载了Sample类,把Sample类的类型信息存放在方法区里。
2)好啦,资料找到了,下面就开始干活啦。Java虚拟机做的第一件事情就是在堆区中为一个新的Sample实例分配内存, 这个Sample实例持有着指向方法区的Sample类的类型信息的引用。这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址,其实,就是有点类似于C语言里的指针啦~~,而这个地址呢,就存放了在Sample实例的数据区里。
3)在JAVA虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。OK,原理讲完了,就让我们来继续我们的跟踪行动!位于“=”前的Test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,它被会添加到了执行main()方法的主线程的JAVA方法调用栈中。而“=”将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。
到这里为止呢,JAVA虚拟机就完成了这个简单语句的执行任务。
接下来,JAVA虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后依次执行它们的printName()方法。当JAVA虚拟机执行test1.printName()方法时,JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,再根据Sample实例持有的引用,定位到方法去中Sample类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指令。
答案部分:8)队列
1、什么是Java优先级队列(Priority Queue)?
答:
-
定义:
优先级队列是逻辑结构是小根堆,存储结构是动态数组(到达上限,容量自动加一)的集合类。 -
特点:
1)优先级队列里的元素必须有优先级,优先级是前后排序的“规则”,也就是说插入队列的类必须实现内部比较器或拥有外部比较器。
2)优先级队列的拥有小根堆的所有特性。
3)优先级队列不是线程安全的。
4)优先级队列不允许使用null元素。
5)优先级队列本身并非一个有序(从a[0]-a[n]全部升序)序列,只有当你把元素一个个取出的时候,这些取出的元素所排成的序列才是有序序列。原因很简单,优先级队列是一个小根堆,也就是只能保证根节点(a[0])是最小的,其余元素的顺序不能保证(当然,其他元素必须遵守小根堆的特性),当我们取出元素(poll)时,我们只能取出根节点的元素,然后把堆的最后一个元素剪切到根节点(这种取出方式是底层算法规定的,充分利用了堆的特性),然后对所有剩余元素进行建堆,建堆之后根节点元素还是最小的(初始堆中的第二小)。由此特点,我们可以引出另外两个知识点:①优先级队列的迭代器遍历出来的数组是没有排序的,只是个小根堆。②如果我们想得到有序的堆,需要把堆先转为数组,然后arrays.sort(queue.toarray),arrays.sort(queue.toarray,comparator对象)或者其他sort方法。
6)优先级队列(堆)中的插入就只能插到最后,也就是说添加和插入一个意思;删除也只能删第一个。
注: 每个元素的优先级根据问题的要求而定。当从优先级队列中取出一个元素后,可能出现多个元素具有相同的优先权。在这种情况下,把这些具有相同优先权的元素视为一个先来先服务的队列,按他们的入队顺序进行先后处理。 -
常用方法:
添加(插入): public boolean add(E e)
查看(只返回根节点元素,不删除): public E peek()
取出(返回根节点元素,会删除源数据): public E poll()
删除(如果有多个相同元素,只会删除第一个): public boolean remove(Object o)
还有就是一些collection类通有的方法,不多说了
注意:所有会破坏堆的特性的方法(比如插入删除等)的源码里最后都会加一个建堆方法(siftUp(i, e),也可以说交换方法,调整方法),使队列保持堆的特性。