一:树
1. 折纸问题
【题目】 请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时 折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2 次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。给定一 个输入参数N,代表纸条都从下边向上方连续对折N次,请从上到下打印所有折痕的方向。 例如:N=1时,打印: down N=2时,打印: down down up
/**
* 折纸问题,就是一棵左结点为down,右结点为up的二叉树
*/
public class Paper {
static class TreeNode{
private String val;
private TreeNode left;
private TreeNode right;
public TreeNode(String val) {
this.val = val;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", left=" + left +
", right=" + right +
'}';
}
}
public static void print(int N){
if (N < 1)
return;
TreeNode root = new TreeNode("down");
build(root,N);
print(root);
}
private static void print(TreeNode node){
if (node.left != null)
print(node.left);
System.out.println(node.val);
if (node.right != null)
print(node.right);
}
private static void build(TreeNode node,int N){
if (N <= 1)
return;
node.left = new TreeNode("down");
node.right = new TreeNode("up");
build(node.left,N-1);
build(node.right,N-1);
}
public static void main(String[] args) {
print(3);
}
}
2. 二叉树先序、中序,后序遍历的非递归实现
//递归前序遍历
public static void pre1(TreeNode node){
if (node == null)
return;
System.out.printf(node.val+" ");
pre1(node.left);
pre1(node.right);
}
//非递归前序遍历
public static void pre2(TreeNode node){
if (node == null)
return;
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || node != null){
//把所有左结点加入到栈
if (node != null){
System.out.printf(node.val+" ");
stack.push(node);
node = node.left;
}
//左结点都为空,把上一个结点的右结点入栈
else
node = stack.pop().right;
}
}
//递归中序遍历
public static void in1(TreeNode node){
if (node == null)
return;
in1(node.left);
System.out.printf(node.val+" ");
in1(node.right);
}
//非递归中序遍历
public static void in2(TreeNode node){
if (node == null)
return;
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || node != null){
//把所有左结点加入到栈
if (node != null){
stack.push(node);
node = node.left;
}
//左结点都为空,把上一个结点的右结点入栈
else
{
node = stack.pop();
System.out.printf(node.val+" ");
node = node.right;
}
}
}
//递归后序遍历
public static void post1(TreeNode node){
if (node == null)
return;
post1(node.left);
post1(node.right);
System.out.printf(node.val+" ");
}
//非递归后序遍历
public static void post2(TreeNode node){
if (node == null)
return;
ArrayList<TreeNode> nodes = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || node != null){
//把所有右结点加入到栈
if (node != null){
nodes.add(node);
stack.push(node);
node = node.right;
}
//右结点都为空,把上一个结点的左结点入栈
else
node = stack.pop().left;
}
Collections.reverse(nodes);
for (TreeNode node1:
nodes) {
System.out.printf(node1.val+" ");
}
}
3. morris遍历
特点:
- 时间复杂度为O(N),空间复杂度为O(1),并且遍历过程不会改变树的形状
- 对于有左子树的结点,访问两次,对于没有左子树的结点,只访问一次
- 大体上沿着当前结点、左、当前结点、右方向移动
要求:
- cur无左子树,向右移动(左边没路,那我直接向右走)
- cur有左子树,找到左子树的最右结点mostRight
- mostRight的右孩子为空:mostRight = cur,cur向左移动(标记自己来过一次,去左边看看)
- mostRight的右孩子为cur:mostRight = null,cur向右移动(看到自己的标记了,左边我走过了,直接去右边)
/**
* morris遍历
*/
public class MorrisTravel {
static class TreeNode {
private int val;
private TreeNode left;
private TreeNode right;
public TreeNode(int val) {
this.val = val;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", left=" + left +
", right=" + right +
'}';
}
}
public static void inTravel(TreeNode root) {
if (root == null)
return;
TreeNode cur = root;
TreeNode next = null;
while (cur != null) {
next = cur.left;
//有左子树
if (next != null) {
while (next.right != cur && next.right != null)
next = next.right;
//第一次访问,改一下指针,向左走(中断)
if (next.right == null) {
next.right = cur;
cur = cur.left;
continue;
}
//第二次访问,修复指针,向右走↓
else
next.right = null;
}
//左子树遍历完成(左-根-右)
System.out.printf(cur.val + " ");
cur = cur.right;
}
}
public static void preTravel(TreeNode root) {
if (root == null)
return;
TreeNode cur = root;
TreeNode next = null;
while (cur != null) {
next = cur.left;
//有左子树
if (next != null) {
while (next.right != cur && next.right != null)
next = next.right;
//第一次访问,改一下指针,向左走(中断)
if (next.right == null) {
next.right = cur;
//(根-左)
System.out.printf(cur.val + " ");
cur = cur.left;
continue;
}
//第二次访问,修复指针,向右走↓
else
next.right = null;
}
//左子树遍历完成
else
//(根-右)
System.out.printf(cur.val + " ");
cur = cur.right;
}
}
public static void postTravel(TreeNode root) {
if (root == null)
return;
TreeNode cur = root;
TreeNode next = null;
while (cur != null) {
next = cur.left;
//有左子树
if (next != null) {
while (next.right != cur && next.right != null)
next = next.right;
//第一次访问,改一下指针,向左走(中断)
if (next.right == null) {
next.right = cur;
cur = cur.left;
continue;
}
//第二次访问,修复指针,向右走↓
else
{
next.right = null;
//已经走完左边和右边了(左-右-根)
//打印左子树的倒置右结点
reverse(cur.left);
}
}
cur = cur.right;
}
//打印根节点的倒置右结点
//左边和右边都走完了
reverse(root);
}
private static void reverse(TreeNode node){
if (node == null)
return;
TreeNode next = node.right;
TreeNode temp = null;
node.right = null;
while (next != null){
temp = next.right;
next.right = node;
node = next;
next = temp;
}
next = node;
while (next != null){
System.out.printf(next.val + " ");
next = next.right;
}
next = node.right;
node.right = null;
while (next != null){
temp = next.right;
next.right = node;
node = next;
next = temp;
}
}
}
4. 在二叉树中找到一个节点的后继节点
【题目】 现在有一种新的二叉树节点类型如下:
public class Node {
public int value;
public Node left;
public Node right;
public Node parent;
public Node(int data) { this.value = data; }
}
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假设有一 棵Node类型的节点组成的二叉树,树中每个节点的parent指针都正确地指向 自己的父节点,头节点的parent指向null。只给一个在二叉树中的某个节点 node,请实现返回node的后继节点的函数。在二叉树的中序遍历的序列中, node的下一个节点叫作node的后继节点。
public static Node nextNode(Node node){
if (node == null)
return null;
//中序遍历是左-中-右
//如果有右结点,那么就在下一层找,找到右孩子的最左边的结点
if (node.right != null){
node = node.right;
while (node != null && node.left != null) {
node = node.left;
}
return node;
}
//如果没有右结点,那么就只能往上一层查找,找到一个作为左结点的父结点
else
while (node.parent != null && node != node.parent.left)
node = node.parent;
return node.parent;
}
5. 何为前缀树? 如何生成前缀树?
/**
* 前缀树
*/
public class PrefixTree {
static class PrefixNode {
//经过结点的单词个数
private int pass;
//单词长度
private int end;
//下一个结点
private HashMap<Integer, PrefixNode> nexts;
public PrefixNode() {
pass = 0;
end = 0;
nexts = new HashMap<>();
}
@Override
public String toString() {
return "PrefixNode{" +
"pass=" + pass +
", end=" + end +
", nexts=" + nexts +
'}';
}
}
private PrefixNode root;
public PrefixTree() {
root = new PrefixNode();
}
public void insert(String word){
if (word == null || word.length() == 0)
return;
PrefixNode node = root;
PrefixNode next;
for (int i = 0; i < word.length(); i++) {
char s = word.charAt(i);
next = node.nexts.get((int)s);
if (next == null)
next = new PrefixNode();
node.nexts.put((int)s,next);
next.pass++;
node = next;
}
node.end++;
}
public void remove(String word){
if (prefixSize(word) != 0)
{
PrefixNode node = root;
PrefixNode next;
for (int i = 0; i < word.length(); i++) {
char s = word.charAt(i);
next = node.nexts.get((int)s);
if (--next.pass == 0){
node.nexts.put((int)s,null);
return;
}
node = next;
}
node.end--;
}
}
public int prefixSize(String prefix){
PrefixNode node = root;
if (prefix == null || prefix.length() == 0)
return 0;
int index = 0;
while (index < prefix.length() && node != null)
{
char c = prefix.charAt(index++);
node = node.nexts.get((int)c);
}
if (node == null)
return 0;
return node.pass;
}
public int count(String word){
PrefixNode node = root;
if (word == null || word.length() == 0)
return 0;
int index = 0;
while (index < word.length() && node != null)
{
char c = word.charAt(index++);
node = node.nexts.get((int)c);
}
if (node == null)
return 0;
return node.end;
}
@Override
public String toString() {
return "PrefixTree{" +
"root=" + root +
'}';
}
}
打印数组
一个字符串类型的数组arr1,另一个字符串类型的数组arr2。arr2中有哪些字符,是arr1中出现的?请打印
arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印
public static void main(String[] args) {
PrefixTree tree = new PrefixTree();
String[] arr1 = {"abc","abd","bcd"};
String[] arr2 = {"abd","cc","ab"};
//打印数组
for (int i = 0; i < arr1.length; i++) {
tree.insert(arr1[i]);
}
System.out.println("出现的字符:");
for (int i = 0; i < arr2.length; i++) {
if (tree.count(arr2[i]) > 0)
System.out.println(arr2[i]);
}
System.out.println("作为前缀的字符:");
for (int i = 0; i < arr2.length; i++) {
if (tree.prefixSize(arr2[i]) > 0)
System.out.println(arr2[i]);
}
}
二:布隆过滤器
作用:黑名单是一个大文件,用于过滤某个信息。如果信息属于黑名单,则会被过滤;如果信息不属于黑名单,也可能会被失误过滤。
结构:大数组,数组元素是一个bit位
定位信息:
- 定位数组的索引:X/位数
- 定位该索引的位数:X%位数
描黑:
- 准备K个哈希函数
- 将信息分别通过哈希函数,得到K个哈希值,把相应的K个位数描黑
检测:
- 将信息分别通过哈希函数,得到K个哈希值
- 如果相应的位都是黑的,则拦截;如果有一个位不是黑的,则放过
特点:
- 与单样本的大小无关
- 与样本数量和失误率有关
三:一致性哈希
原来的结构
- 客户端发出一个key,请求一个value
- 服务端通过对key的哈希函数求出哈希值,模服务器个数,确定提供服务的服务器X
- 由服务器X提供服务
问题:
- 服务器的增加和删除,数据迁移的代价高
- 改动代码
改进的结构
- 服务端通过对每个服务器的ip求出哈希值,连成环
- 客户端发出一个key,请求一个value
- 服务端通过对key的哈希函数求出哈希值,打到环上,确定环上顺时针方向的下一个服务器X(通过二分法确定)
- 由服务器X提供服务
特点:
- 添加一个服务器X1后,找到顺时针方向的下一个服务器X2,请他把X1-X0的数据传给X1
- 初级服务器个数少时,负载不均衡
- 添加服务器后,负载不均衡
再改进的结构
- 每个服务器分配一定量的虚拟节点,放在路由表
- 服务端通过对每个服务器的每个虚拟节点求出哈希值,连成环
四:哈希表
1. 并查集
- 查找两个元素是否属于同一个集合
- 把两个集合合并为一个大集合
/**
* 并查集
*/
public class UnionSet {
static class Node{
private int val;
public Node(int val) {
this.val = val;
}
}
//记录每个节点的代表节点
HashMap<Node,Node> fathers;
//记录每个代表节点的集合个数
HashMap<Node,Integer> sizes;
public UnionSet(List<Node> nodes){
fathers = new HashMap<>();
sizes = new HashMap<>();
build(nodes);
}
//初始化
private void build(List<Node> nodes){
fathers.clear();
sizes.clear();
for (Node node :
nodes) {
fathers.put(node, node);
sizes.put(node,1);
}
}
//查找代表节点
private Node findPresent(Node node){
Node present = fathers.get(node);
//找出代表节点为本身的节点
if (present != node) {
present = findPresent(present);
}
//找到之后,把其他节点都更新代表节点,加快查询速度
fathers.put(node,present);
return present;
}
//判断是否是同一个集合
public boolean isSame(Node a,Node b){
if (a == null || b == null)
return false;
return findPresent(a) == findPresent(b);
}
//合并集合
public void union(Node a,Node b){
if (a == null || b == null || isSame(a,b))
return;
int size1 = sizes.get(a);
int size2 = sizes.get(b);
if (size1 > size2) {
fathers.put(b, a);
sizes.put(a,size1+size2);
}
else {
fathers.put(a, b);
sizes.put(b,size1+size2);
}
}
}
2. 岛问题
一个矩阵中只有0和1两种值, 每个位置都可以和自己的上、 下、 左、 右四个位置相连
如果有一片1连在一起, 这个部分叫做一个岛, 求一个矩阵中有多少个岛?
举例:
0 0 1 0 1 0
1 1 1 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
这个矩阵中有三个岛
/**
* 岛问题
*/
public class Island {
static int sum = 0;
//遍历岛
public static int getIsland(int[][] matrix){
if (matrix == null || matrix[0] == null)
return 0;
int N = matrix.length;
int M = matrix[0].length;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (matrix[i][j] == 1){
//找到有病毒的岛,开始感染
sum++;
getIsland(matrix,i,j,N,M);
}
}
}
return sum;
}
//不断感染,直到没有病菌
public static void getIsland(int[][] matrix,int x,int y,int N,int M){
if (x < 0 || y < 0 || x >= N || y >= M || matrix[x][y] != 1)
return;
matrix[x][y] = 2;
getIsland(matrix,x+1,y,N,M);
getIsland(matrix,x-1,y,N,M);
getIsland(matrix,x,y+1,N,M);
getIsland(matrix,x,y-1,N,M);
}
public static void main(String[] args) {
int[][] matrix = {{0,0,1,0,1,0},{1,1,1,0,1,0},{1,0,0,1,0,0},{0,0,0,0,0,0}};
System.out.println(getIsland(matrix));
}
}
2. 随时找到数据流的中位数
【题目】 有一个源源不断地吐出整数的数据流,假设你有足够的空间来 保存吐出的数。请设计一个名叫MedianHolder的结构, MedianHolder可以随时取得之前吐出所有数的中位数。
【要求】
- 如果MedianHolder已经保存了吐出的N个数,那么任意时刻 将一个新数加入到MedianHolder的过程,其时间复杂度是 O(logN)。
- 取得已经吐出的N个数整体的中位数的过程,时间复杂度为 O(1)
/**
* 保存中位数
*/
public class MedianHolder {
class MyComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
if (o1 > o2)
return -1;
else if(o1 < o2)
return 1;
else
return 0;
}
}
//最小堆和最大堆
PriorityQueue<Integer> min = new PriorityQueue<>();
PriorityQueue<Integer> max = new PriorityQueue<>(new MyComparator());
//最小堆只添加比自己大的数字,这样才不用调整
//最大堆只添加比自己小的数字,或者比自己大又比其他小的数字
//维持两个堆的平衡
public void add(int num){
if (max.isEmpty() || num <= max.peek())
max.add(num);
else if (min.isEmpty() || num >= min.peek())
min.add(num);
else
max.add(num);
modify();
}
//保证两个堆最多相差一个
private void modify(){
if (min.size() - max.size() > 1)
max.add(min.poll());
if (max.size() - min.size() > 1)
min.add(max.poll());
}
//奇数个数字,则在多一个数量的堆里面
//偶数个数字,取中间值
public int get(){
if (max.isEmpty() && min.isEmpty())
return Integer.MIN_VALUE;
if (min.isEmpty())
return max.peek();
if (max.isEmpty())
return min.peek();
int minSize = min.size();
int maxSize = max.size();
if ((maxSize+minSize) % 2 == 0)
return (max.peek() + min.peek())/2;
else if (maxSize > minSize)
return max.peek();
else
return min.peek();
}
}