散列表(上)
散列表是基于数组的随机访问实现的,本身是数组的一种扩展。
散列函数
hash(key),根据关键字获得对应散列表的下标,hash即散列函数。散列函数设计的三大要求:
- 散列函数计算得到的散列值是一个非负整数;
- 如果key1 = key2,那hash(key1) == hash(key2);
- 如果key1 ≠ key2,那hash(key1) ≠ hash(key2)。 其中第三点是无法实现的,无论多好的散列函数都会出现hash冲突。
散列冲突
装载因子: 散列表的装载因子=填入表中的元素个数/散列表的长度,用于表示散列表中空闲位置的多少,装载因子越大,说明空闲位置越小,冲突越大。
解决方法
1、开放寻址法。以线性探测法为例,每次添加数据时,如果当前下标已经有值,就向后查找,直到有空闲位置。查找类似添加功能。删除功能不能直接删除数据,而是添加一个delete标识,否则当某一数据删除时,在线性查找一个数据时,会由于遇到空数据而提前停止查找。
优点:可有效利用cpu缓存,提高查询速度,序列化简单。
缺点:当插入的数据越多时,其发生hash冲突的概率就越大,最差情况下时间复杂度为O(n)。删除数据时需要使用标记,因此装载因子上限不能太大。这导致他比链表法更浪费空间。
ThreadLocalMap内部就采用开放寻址法
2、链表法
插入时,直接插入到指定位置的尾部即可,时间复杂度为O(1),查找和删除的时间复杂度和链表长度k成正比,k = m/N(m表示数据的个数,N表示槽的个数)。
优点:对装载因子容忍度高。
缺点:指针需要额外空间。无法利用cpu缓存
思考
- 假设我们有10万条URL访问日志,如何按照访问次数给URL排序?
- 有两个字符串数组,每个数组大约有10万条字符串,如何快速找出两个数组中相同的字符串
解答:
1、使用散列表,key为url,value为访问次数,当数据量比较小时,可以使用计数排序,当数据量大时使用快排。
2、将第一个数组构建成散列表,key为字符串,即可判断第二个数组中的字符串是否出现在第一个数组中。
散列表(中)
为什么要打造一个好的散列表?
如果散列函数设计得不好,或者装载因子过高,都可能导致散列冲突发生的概率升高,查询效率下降。 在极端情况下,有些恶意的攻击者,还有可能通过精心构造的数据,使得所有的数据经过散列函数之后,都散列到同一个槽里。如果我们使用的是基于链表的冲 突解决方法,那这个时候,散列表就会退化为链表,查询的时间复杂度就从O(1)急剧退化为O(n)。
如何设计散列表
首先,散列函数的设计不能太复杂。过于复杂的散列函数,势必会消耗很多计算时间,也就间接的影响到散列表的性能。其次,散列函数生成的值要尽可能随机 并且均匀分布,这样才能避免或者最小化散列冲突,
如何解决装载因子过大问题?
对于静态数组,只需要确认数据的规模,设置一个较好的装载因子阈值即可。
对于动态数组,可以进行必要的扩容操作,当数据过大时,申请一个更大的新数组,将旧的数据迁移到新数组中。通过摊还分析法,扩容的平均时间复杂度为O(1).
实际上,对于动态散列表,随着数据的删除,散列表中的数据会越来越少,空闲空间会越来越多。如果我们对空间消耗非常敏感,我们可以在装载因子小于某个 值之后,启动动态缩容。当然,如果我们更加在意执行效率,能够容忍多消耗一点内存空间,那就可以不用费劲来缩容了
装载因子阈值的设置要权衡时间、空间复杂度。如果内存空间不紧张,对执行效率要求很高,可以降低负载因子的阈值;相反,如果内存空间紧张,对执行效率 要求又不高,可以增加负载因子的值,甚至可以大于1。
如何高效扩容?
为了解决一次性扩容耗时过多的情况,我们可以将扩容操作穿插在插入操作的过程中,分批完成。当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。
当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,我们都重复上面的过 程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次性数据搬移,插入操作就都变得很快了。
对于查询操作,为了兼容了新、老散列表中的数据,我们先从新散列表中查找,如果没有找到,再去老的散列表中查找。
小结
一个工业级散列表应该满足的要求:
- 支持快速的查询、插入、删除操作;
- 内存占用合理,不能浪费过多的内存空间;
- 性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况。
设计思路:
- 设计一个合适的散列函数;
- 定义装载因子阈值,并且设计动态扩容策略;
- 选择合适的散列冲突解决方法
散列表(下)
LRU实现
package algorithm;
import java.util.HashMap;
import java.util.Map;
/**
* @author EnochStar
* @title: LRU
* @projectName basic_use
* @description: TODO
* @date 2021/10/30 10:59
*/
public class LRU<K,V> {
private final static int DEFAULT_CAPACITY = 10;
private int capacity;
private DNode<K,V> head;
private DNode<K,V> tail;
private int length;
private Map<K,DNode<K,V>> table;
public LRU(int capacity) {
this.capacity = capacity;
head = new DNode<>();
tail = new DNode<>();
head.next = tail;
tail.pre = head;
length = 0;
table = new HashMap<>();
}
public LRU() {
this(DEFAULT_CAPACITY);
}
public void add(K key,V value) {
DNode<K, V> node = table.get(key);
if (node == null) {
node = new DNode<>(key,value);
table.put(node.key,node);
addNode(node);
if (++length > capacity) {
DNode<K, V> tailNode = removeTail();
table.remove(tailNode.key);
length--;
}
}else{
node.value = value;
removeToHead(node);
}
}
public DNode<K,V> getNode(K key){
DNode<K, V> node = table.get(key);
if (node == null) return null;
else {
removeToHead(node);
return node;
}
}
public void remove(K key) {
DNode<K,V> node = table.get(key);
if (node == null) return;
else{
removeNode(node);
table.remove(key);
length--;
}
}
public void printAll() {
DNode<K, V> node = head.next;
while (node.next != null) {
System.out.print(node.value + ",");
node = node.next;
}
System.out.println();
}
public DNode<K,V> removeTail(){
DNode<K,V> node = tail.pre;
removeNode(node);
return node;
}
public void removeToHead(DNode<K,V> node) {
removeNode(node);
addNode(node);
}
public void removeNode(DNode<K,V> node){
node.pre.next = node.next;
node.next.pre = node.pre;
}
public void addNode(DNode<K,V> node) {
node.next = head.next;
head.next.pre = node;
head.next = node;
node.pre = head;
}
}
class DNode<K,V>{
public K key;
public V value;
public DNode<K,V> pre;
public DNode<K,V> next;
public DNode() {
}
public DNode(K key, V value) {
this.key = key;
this.value = value;
}
}
哈希算法
概念:将任意长度的字符串映射为固定长度的二进制串。哈希算法基本要求:
- 只支持单向哈希
- 哈希算法必须运行高效
- 尽可能使发生哈希冲突的概率低
- 具有敏感性,任意字符修改,都会导致哈希值的改变
应用
安全加密,基于单向哈希和哈希冲突概率低
唯一标识,当要判断某一张图在图库中是否存在时,比较笨的方法是依次比较图库中的图,另一种方法是可以给每个图的二进制串,取固定值然后求哈希值,在判断是否存在时,只需要比对哈希值即可。
数据校验,基于敏感性
散列函数,要求算法实现高效,数据能均匀分布
思考:了解区块链吗?
解答:区块链是由一个个区块组成,区块包括区块体和区块头,区块头存储自己的区块体和上一个区块头的哈希值,哈希算法比较耗时,当某一个区块被修改了,后续的区块头存储的内容就不对了,所以短时间修改区块链是比较困难的。
哈希算法(二)
哈希算法还可以运用到分布式系统当中。
应用
负载均衡,哈希算法可以实现一个会话粘滞的哈希算法,即将一个会话id对应到一个服务器,这一功能可以用哈希表去实现,但是当数据较大时,会消耗过多内存,同时,客户频繁的上线下线会使维护映射表的成本过大。可以用哈希算法对会话id进行计算,使每一个会话id定位到一个服务器。
数据分片,在数据量很大的情况下要计算某一关键字出现的次数,有两大难点,一是搜索日志很大,无法全部放入内存,二是只用一台机器会导致计算时间过长。可以使用哈希算法对关键字求值,然后取模,分配到对应的机器上,这样的话求某一关键字出现的次数只需要在对应的机器上计算即可。
分布式存储,利用一致性哈希,可以使新加入机器时不用进行大规模的数据迁移,只需要对部分数据进行迁移即可。
二叉树(一)
基本概念
节点的高度:节点到叶子节点的最长路径(边数)
节点的深度:根节点到该节点所经历的边的个数
节点的层数:节点的深度+1
二叉树存储
1、基于链表存储
2、基于数组存储
将根节点存储在下标为i的位置,左节点存储在2*i的位置,右结点存储在2 * i + 1的位置。
显然这种方法如果存储的是完全二叉树的话,只会浪费下标为0的空间,而存储非完全二叉树的话,会浪费过多空间。
二叉树的遍历
中序遍历:
/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb) {
if (root == null) {
return;
}
serialize(root.left, sb);
/****** 中序遍历位置 ******/
sb.append(root.val).append(SEP);
/***********************/
serialize(root.right, sb);
}
前序遍历:
/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb) {
if (root == null) {
return;
}
/****** 前序遍历位置 ******/
sb.append(root.val).append(SEP);
/***********************/
serialize(root.left, sb);
serialize(root.right, sb);
}
后序遍历:
/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb) {
if (root == null) {
return;
}
serialize(root.left, sb);
serialize(root.right, sb);
/****** 后序遍历位置 ******/
sb.append(root.val).append(SEP);
/***********************/
}
层次遍历:
public void LevelTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
while (size > 0) {
TreeNode node = queue.poll();
// to do something with node
System.out.println(node.val);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
size--;
}
}
}
二叉树(二)
二叉查找树是一种特殊的二叉树,支持动态数据集合的快速插入、查找、删除。其特点是树中的任意节点,其左子树每个节点的值都小于该节点的值,其右子树每个节点的值都大于该节点的值。
查找操作
public TreeNode search(TreeNode root,int val){
if (root == null) return null;
if (root.val == val) {
return root;
}else if (root.val > val) {
return search(root.left,val);
}else{
return search(root.right,val);
}
}
插入操作
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root == null) {
return new TreeNode(val);
}
if(val > root.val) {
root.right = insertIntoBST(root.right,val);
}
if(val < root.val) {
root.left = insertIntoBST(root.left,val);
}
return root;
}
删除操作
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return null;
if (root.val == key) {
if (root.left == null) return root.right;
if (root.right == null) return root.left;
TreeNode node = getMin(root.right);
root.val = node.val;
root.right = deleteNode(root.right,node.val);
}else if (root.val > key) {
root.left = deleteNode(root.left,key);
}else {
root.right = deleteNode(root.right,key);
}
return root;
}
public TreeNode getMin(TreeNode root) {
while (root.left != null) root = root.left;
return root;
}
二叉搜索树的特性
中序遍历时的数据是有序的
包含重复数据的二叉查找树
当用二叉查找树存储重复键时,有两种方法。
1、每个结点为<K,[V]>类型,对于相同键的值,将其存储在数组中(链表也行)
2、对于重复键的数据,就将其添加到当前节点的右侧,再进行比较决定插入的位置,缺点在于查找数据时,尽管查找到了对应的键,仍要继续查找。删除操作也是如此。
时间复杂度分析
当二叉查找树平衡时,则查找的效率与树高有关为O(logn),当二叉查找树极度不平衡时,就会退化为链表,时间复杂度为O(n)。
为什么需要二叉查找树
散列表的增删改只需要O(1)的时间复杂度,而二叉查找树的效率不稳定,且最优情况下才O(logn),那么为什么还要二叉查找树呢?
- 散列表是无序的,而二叉查找树只需要中序遍历即可得到有序数据。
- 散列表需要考虑哈希函数的设计,当遇到哈希冲突时,散列表的操作也是不稳定的,而平衡二叉查找树的效率是稳定的
- 一个优秀的散列表并不好设计,需要注意扩容,散列函数,哈希冲突的解决思路,缩容等问题,而二叉查找树只需要解决平衡问题即可
思考
用编程实现获得二叉查找树的高度。
答:
public int getHeight(TreeNode root) {
if (root == null) return 0;
int height = 1 + Math.max(getHeight(root.left),getHeight(root.right));
return height;
}
红黑树
红黑树是非严格的平衡二叉查找树。平衡二叉树中任意节点的左右子树相差不能大于1。AVL树是严格平衡的二叉查找树。
红黑树的定义
- 根节点是黑色的
- 每个叶子节点都是黑色的空节点
- 任意相邻节点不能同时为红色
- 每个节点到根节点都经过相同数量的黑色节点
红黑树的近似平衡?
将红黑树中的红色节点剔除,若一个节点的父节点为空,则取其祖父节点作为父节点。
由红黑树的性质每个节点到根节点都经过相同数量的黑色节点可知,从上图四叉树中取出某些节点放到下一层的节点,即形成了完全二叉树,显然上图的四叉树的高度是低于完全二叉树的高度的,黑色节点的个数为n,则完全二叉树高度为logn,所以四叉树的高度小于logn,当把红色节点再加入时,由于一个黑色节点最多相邻一个红色节点,所以红黑树的整体高度不大于2logn。
既然有AVL树,为什么还要选择红黑树呢?
AVL树的维护成本比红黑树大,二者的插入删除查找操作都很稳定,所以偏向红黑树。
堆
什么是堆
堆是特殊的树,需要满足两个要求:
- 堆是一个完全二叉树
- 堆中每一个节点的值必须大于等于(小于等于)其子树每个结点的值
实现堆
由于堆是完全二叉树,因此采用数组存储数据。 其插入、删除、建堆、排序的代码如下:
package algorithm;
/**
* @author EnochStar
* @title: Heap
* @projectName basic_use
* @description: TODO
* @date 2021/11/4 10:33
*/
public class Heap {
// 数组的容量
private int capacity;
private int[] nums;
// 数组内存在的元素个数
private int count;
public Heap(int capacity) {
this.capacity = capacity;
nums = new int[capacity + 1];
count = 0;
}
public boolean insert(int num) {
if (count >= capacity) return false;
count++;
nums[count] = num;
int i = count;
while (i / 2 > 0 && nums[i] > nums[i / 2]) {
swap(nums,i,i / 2);
i = i / 2;
}
return true;
}
public boolean deleteTop() {
if (count == 0) return false;
nums[1] = nums[count];
count--;
heapyfy(nums,count,1);
return true;
}
// 自上而下的堆化 堆化是顺着树向下走的,因此时间复杂度不超过O(logn)
public void heapyfy(int[] nums,int count,int start) {
while (true) {
int maxPos = start;
if (start * 2 <= count && nums[start] < nums[2 * start]) {
maxPos = 2 * start;
}
if (2 * start + 1 <= count && nums[maxPos] < nums[2 * start + 1]) {
maxPos = 2 * start + 1;
}
if (maxPos == start) break;
swap(nums,maxPos,start);
start = maxPos;
}
}
public void buildHeap(int[] nums,int count) {
for (int i = count / 2;i >= 0;i--) {
heapyfy(nums,count,i);
}
}
public void sort(int[] nums,int count) {
int k = count;
while (k > 1) {
swap(nums,k,1);
k--;
heapyfy(nums,k,1);
}
}
public void swap(int[]nums,int s1,int s2){
int tmp = nums[s1];
nums[s1] = nums[s2];
nums[s2] = tmp;
}
}
插入是自下而上的堆化,删除和建堆是自上而下的堆化。由于插入删除操作时间主要消耗在堆化上,堆化的时间复杂度与树的高度成正比,因此堆化的时间复杂度为O(logn)。建堆操作,每一个节点比较和交换与节点高度成正比,因此每一层的时间复杂度为(该层节点个数 * 节点高度),计算可知时间复杂度为O(n)。 排序的时间复杂度为O(nlogn)。
堆排非稳定排序,属于原地排序。
快排与堆排
1、快排是顺序访问的,而堆排是跳着访问的,因此快排对cpu缓存更加友好
2、堆排交换次数比快排多
堆的应用
优先队列
堆可以用于实现优先队列。
合并有序小文件
假设有100个小文件,每个小文件内存储的是有序字符串,现要将这些小文件合并成一个有序的大文件。
可以采用归并排序的思路,依次取出每个文件的第一个放入数组中比较,然后取最小的放入新文件中,再从数组中删除。
如果采用数组的话,需要遍历数组找出最小的,时间复杂度为O(n),而如果使用优先队列的话,其插入和删除只需要O(logn)的时间复杂度。
高性能定时器
假设定时器维护了很多定时任务,定时器每过一个很小的时间单位,就去遍历定时任务,判断是否需要执行。
这种方法有两个弊端: 1、任务执行的时间可能距离当前时间还要很久,那么这个遍历是徒劳的 2、如果任务队列很长,那么遍历会非常耗时
可以将定时任务放入优先队列当中,然后取出优先队列的第一个,获取距离任务执行时间,当任务执行时间到的适合,再从优先队列中取出该任务。
求topk
如果求最大的k个元素,可以维护一个大小为k的小顶堆,如果新插入的元素比堆顶元素大,就删除堆顶元素,插入新的元素,否则,就不变。时间复杂度:遍历数组需要O(n),堆化的时间复杂度为O(logk),最差情况是n个数据都需要堆化,时间复杂度为O(nlogk)
package algorithm;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
/**
* @author EnochStar
* @title: TopK
* @projectName basic_use
* @description: TODO
* @date 2021/11/4 15:43
*/
public class TopK {
public int[] topKFrequent(int[] nums, int k) {
PriorityQueue<Map.Entry<Integer, Integer>> priority =
new PriorityQueue(k, new Comparator<Map.Entry<Integer, Integer>>() {
@Override
public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) {
return o1.getValue() - o2.getValue();
}
});
Map<Integer, Integer> map = new HashMap();
for (int i : nums) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (priority.size() < k) {
priority.add(entry);
} else {
if (priority.peek().getValue() < entry.getValue()) {
priority.poll();
priority.add(entry);
}
}
}
int[] top = new int[k];
int index = 0;
while (!priority.isEmpty()) {
top[index++] = priority.poll().getKey();
}
return top;
}
}
求中位数
可以维护两个堆,一个大顶堆,一个小顶堆,小顶堆的任意元素都比大顶堆的大。如果是奇数的话,可以令大顶堆存放n/2+1个元素,小顶堆存放n/2个元素,那么中位数就是大顶堆的堆顶元素;如果是偶数的话,那么分别存放n/2个元素。
当数据是动态变化的情况,如果新插入的数据比大顶堆小,就插入到大顶堆中,如果比小顶堆大,就插入到小顶堆当中,为了保证两个堆中数据量的比值保持一定,需要动态的将数据进行迁移(即从一个堆迁移到另一个堆)。
package algorithm;
import java.util.PriorityQueue;
/**
* @author EnochStar
* @title: MidNum
* @projectName basic_use
* @description: TODO
* @date 2021/11/4 14:48
*/
class MedianFinder {
PriorityQueue<Integer> smallHeap;
PriorityQueue<Integer> bigHeap;
/** initialize your data structure here. */
public MedianFinder() {
smallHeap = new PriorityQueue<Integer>();
bigHeap = new PriorityQueue<Integer>((o1,o2) -> o2 - o1);
}
public void addNum(int num) {
// 保持平衡 即小顶堆的所有元素均大于大顶堆
bigHeap.add(num);
smallHeap.add(bigHeap.poll());
// 使其相差最大为1
if(smallHeap.size() > bigHeap.size() + 1) {
bigHeap.add(smallHeap.poll());
}
}
public double findMedian() {
if(smallHeap.size() > bigHeap.size()) return smallHeap.peek();
else return (double)(bigHeap.peek()+smallHeap.peek())/2;
}
}
思考
假设有10亿条数据,如果快速获得搜索热度最高的数据。首先可以使用哈希函数将数据分别放入10个文件中,然后使用散列表存储关键词及其搜索频率,在10个文件中分别建大小为10的小顶堆,最后合并即可。
图
图是非线性表,包括有向图和无向图
由于好友是双向的,所以一般用无向图,而博主与粉丝是单向的,所以一般用有向图。有些场景下需要有双方的亲密度,这个时候只需要在关系上加权值即可,带权图。
图的存储方式
1、邻接矩阵。
显然邻接矩阵对于无向图来说会浪费一半的空间,如果存储的是稀疏图,那么浪费的空间更多,其优点是简单直接,获取顶点间的关系非常直接。
2、邻接表。
邻接表是用空间换时间,不过可以对其进行优化,比如可以用跳表、散列表、红黑树等进行优化。
如何设计一个微博?
- 大致思考微博需要的一个功能:
- 判断用户A是否为用户B的粉丝
- 判断用户B有无被用户A关注
- 获取当前用户的粉丝
- 获取当前用户关注的人
- 对粉丝和关注人按名字进行排序
社交网络是一个稀疏图,所以使用邻接矩阵会浪费大量的空间,因此推荐邻接表,假设邻接表存储当前用户的粉丝,那么该如何查找当前用户关注的人?可以再加一个逆邻接表,用于存储关注的用户。
由于邻接表遍历的时间复杂度为O(n),可以对邻接表进行优化,如红黑树、跳表、散列表,以提高查询效率,微博需要一个排序的功能,考虑设计的简便,可以使用跳表来优化。这样查询排序的时间复杂度都为O(logn)。
当数据量很大的时候,无法完全将数据存放在内存中。有两种方式,第一种是将数据存放在硬盘中,第二种方式就是添加机器,无论哪种方式,都根据哈希函数将数据映射到对应的数据库(机器)上。
深搜和广搜
广搜
时间复杂度为O(V+E),V为顶点个数,E为边数,对应连通图而言V <= E-1,所以时间复杂度为O(E), 由于需要辅助数组等存放顶点,所以空间复杂度为O(V)
深搜
采用了回溯的思想,每条边最多遍历两次,故时间复杂度为O(E),内部会使用辅助数组等,同时递归调用栈的最大深度不超过顶点的个数,因此空间复杂度为O(V)。
具体实现
package algorithm;
import java.util.LinkedList;
import java.util.Queue;
/**
* @author EnochStar
* @title: Graph
* @projectName basic_use
* @description: TODO
* @date 2021/11/5 14:53
*/
public class Graph {
private int v;
private LinkedList<Integer>[] adj;
public Graph(int v) {
this.v = v;
adj = new LinkedList[v];
for (int i = 0;i < v;i++) {
adj[i] = new LinkedList<>();
}
}
// 无向图
public void addEdge(int sour,int obj) {
adj[sour].add(obj);
adj[obj].add(sour);
}
public void bfs(int start,int end){
int[] prev = new int[v];
for (int i = 0;i < v;i++) {
prev[v] = -1;
}
boolean[] visited = new boolean[v];
visited[start] = true;
Queue<Integer> queue = new LinkedList();
queue.add(start);
while (!queue.isEmpty()) {
int s = queue.poll();
for (int i = 0;i < adj[s].size();i++) {
int num = adj[s].get(i);
if (!visited[num]) {
prev[num] = s;
if (num == end) {
printRoad(prev,start,end);
return;
}
visited[num] = true;
queue.add(num);
}
}
}
}
private void printRoad(int[] prev,int start,int end){
while (end != start && prev[end] != -1) {
printRoad(prev,start,prev[end]);
}
System.out.println(end + " -> ");
}
public void dfs(int start,int end) {
int[] prev = new int[v];
boolean[] visited = new boolean[v];
for (int i = 0;i < v;i++) {
prev[i] = -1;
}
boolean found = false;
dfsRecur(start,end,visited,prev,found);
printRoad(prev,start,end);
}
private void dfsRecur(int start,int end,boolean[] visited,int[] prev,boolean found) {
if (found) return;
visited[start] = true;
if (start == end) {
found = true;
return;
}
for (int i = 0;i < adj[start].size();i++) {
int num = adj[start].get(i);
if (!visited[num]) {
prev[num] = start;
dfsRecur(num,end,visited,prev,found);
}
}
}
}