Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions CourseSchedule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//
// CourseSchedule.swift
// DSA-Practice
//
// Created by Paridhi Malviya on 1/19/26.
//

/*
(1, 0) -> in order to do course 1, we should have finished course 0.
prerequisites array -> [[1,0], [], []] -> if we draw, we will figure out that it's edges array.
chekc if 0,1,2,3,4,5 -> courses can be finished.
initially we can

=> How to identidy if it's a graph problem or not ?
start drawing. If we find connections..

Different ways to represent graph - adjacency list or adjacency matrix, an array of edges.

choose an independent one. process it.
others which becomes independet then next will b eindependent...so on and so forth -
Topological sort
indegrees array -> how many sharp edges are incoming to a new node.

all the babies of an independent node has to be processed.
use BFS. use queue.
how to find that what all items should be added to the queue when doing BFS.

--> if we process 0 then search the edges array for dependent once.
search is leanr.
so for search optimization , use hashing baed data structure. Hence, hash map.

use independetn to dependent list.
if added children t queue. reduce indegree by 1. if it turned into 0 means it has become independent, If not, not yet become independent.

Taking inside the queue means we can complete the course. If all indegrees are 0 then

-> two ways of representing a graph.
hash map - 1 is dependt on 0.
4 has outgoinf to 5

which format is applkicable for out problem is critical decision.


level up ->
If indegree of two elements are 0, then
if we put both i queue -> then it will
if we don't put both - thne it will not. if all independent nodes are not going i queue then it will not work.
for BFS - all sould be processed. allnodes of particular level hosul be processed together.
DFS - we can use any

********** all nodes of particular level hosul be processed together in BFS.

Toplological sort is used to check a cycle. Somenodes will not be gong inside the queue. If thetre is cyclic dependency then we can identify it.

Using DFS also, we can detect the cycle.
topological sort ->


time complexity of the solution -> O(V + E). which ever is bigger, that will be considered.
space compelxityt -> adjacency list O(V + E)
from hash map. we are picking all eges for a particular vertex, we are processing al, those edges. thats;w combinly called E
No of edges are greater than no of vertices.
In worst case, edges can go upto n^2.
E - consolidation of all children, all edges.

*/

//toplological sort solution
class CourseSchedule {

init() {

}

func canFinish(_ numCourses: Int, _ prerequisites: [[Int]]) -> Bool {

var indegrees = Array(repeating: 0, count: numCourses)
var adjacencyMap = [Int:[Int]]()
for course in prerequisites {
//for each course, set the indegrees
let independent = course[1]
let dependent = course[0]

if (adjacencyMap[course[1]] != nil) {
adjacencyMap[course[1]]!.append(dependent)
} else {
adjacencyMap[course[1]] = [dependent]
}
indegrees[dependent] += 1
}

//topological sort started
//add the courses for which indegrees are 0
var queue = QueueUsingLL<Int>()
var count = 0 //how many nodes are going inside the queue
for (index, value) in indegrees.enumerated() {
if (value == 0) {
queue.enqueue(index)
count += 1
}
}

if (count == numCourses) {
//if all courses has gone into the queue so all are taken.
return true
}
//If queue is empty initially, it means there is a cycle. Nothing will go inside the queue
if (queue.isEmpty) {
return false
}

while (queue.size != 0) {
let curr = queue.dequeue()
if let curr = curr {
//check in adjacency list the connections from this node
if let children = adjacencyMap[curr] {
for val in children {
indegrees[val] -= 1
if (indegrees[val] == 0) {
queue.enqueue(val)
//whenever putting inside the queue, increment the count by 1
count += 1
if (count == numCourses) {
return true
}
}
}
}
}
}
return false
}
}

174 changes: 174 additions & 0 deletions LevelOrderTraversal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//
// LevelOrderTraversal.swift
// DSA-Practice
//
// Created by Paridhi Malviya on 1/19/26.
//

/*
Level order traversal -

Inorder, preorder, postorder -> DFS based algorithm.
BFS based traversal.

DFS is -> any one child is processed at a time
BFS -> all children are processed at once.

In DFS, if I want to process all children at once, then we need to process level wise. FIFO. Hence, queue is used. with the vision of processing all children together, maintain FIFO order.

On some BFS solution -> we maintain size. In some, we don't
siz evariable -> If we need distinction that what all nodes belong to which level, then maintain size variable of the queue.
*****When level finishes, then take the size of the queue.
If we are not sure if we should take the size of the queue or not, always take the size of the queue.

Initial size of the queue - -> at level 1, we have 1 node. size = 1
process all cihldren of the node -> at level 2, 3 and 1 -> current level finished. The take the sizeof the queue.
process all nodes of a level. then take the size of the queue. -> these are the number of nodes at the next level.

Time complexity -> O(n) all nodes are touched once and going inside the queue.
space complexity -> maximum no of leaf nodes - (n/2) 50% of the nodes are at the leaf level.
1/2 doesn't matter, so n/2

For each data structure -> size(), isEmpty() - these fucntions will always be O(1)

*/


class LevelOrderTraversal {

init() {
let listOfLevels = levelOrderTraversal(root: TreeNode(val: 5, left: TreeNode(val: 4, left: nil, right: nil), right: TreeNode(val: 3, left: nil, right: nil)))
print("list of levels \(listOfLevels)")
}

func levelOrderTraversal(root: TreeNode?) -> [[Int]] {
var result: [[Int]] = [[Int]]()
guard let root = root else {
return [[]]
}
var queue = QueueUsingLL<TreeNode>()
queue.enqueue(root)

while (!queue.isEmpty) {
//maintain size of the queue
let size = queue.size

var listOfNodesAtALevel: [Int] = []
//process the level

for i in 0..<size {

let currNode = queue.dequeue()
if let currNode = currNode {
listOfNodesAtALevel.append(currNode.val)
//process babies
if let leftNode = currNode.left {
queue.enqueue(leftNode)
}
if let rightNode = currNode.right {
queue.enqueue(rightNode)
}
}
}
result.append(listOfNodesAtALevel)
}
return result
}

func levelOrderTraversalWithoutElementsSegregation(root: TreeNode?) {
guard let root = root else {
print("root is nil")
return
}
var queue = QueueUsingLL<TreeNode>()
queue.enqueue(root)
var listOfNodes: [Int] = []

while (!queue.isEmpty) {
let currNode = queue.dequeue()
if let currNode = currNode {
listOfNodes.append(currNode.val)

//process babies
if let leftNode = currNode.left {
queue.enqueue(leftNode)
}
if let rightNode = currNode.right {
queue.enqueue(rightNode)
}
}
}
print("listOfNodesAtALevel \(listOfNodes)")
}
}


/*
=> Level order traversal using DFS ->
send out the level variable as a parameter of recursion,
Use hash map -> each level numbers as key of dictionary. That level's data as the value of that keys in a n array.
O(n) - extra space. challenge -> not sorted keys.


TreeMap and orderMaps are under the hood self-balanced BST. Time complexity to search for an element - log(n)
sorting the keys - n log n
using hashing map with priority queue- nlogn

=> maintain a minimum and maximum depth. I can linearly traverse through it then. -> Bucket sort. O(n) time complexity.
extra space -> O(n)

build the above hashmap into the result array itself.
=> maintain list of list. [[], [], []]. level 0 items will be added at 0th indexed list, level 1 items will be added to 1st indexed list and so on...
tmie complexity -. O(n)
space -> O(h) for recursion call stacks. over the hood - O(1) because only output array.

*/
class LevelOrderTraversalUsingDFS {

func levelOrder(_ root: TreeNode?) -> [[Int]] {
var result: [[Int]] = [[Int]]()
dfs(root: root, level: 0, result: &result)
return result
}

private func dfs(root: TreeNode?, level: Int, result: inout [[Int]]) {

//base
guard let root = root else {
return
}
//logic
if (level == result.count) {
//it means the array corresponding to this level doesn't exist in the 2-D array
result.append([])
}
//can add the root at the post order, preorder, inorder level. But the list should be appended at the preorder level.
result[level].append(root.val)

dfs(root: root.left, level: level + 1, result: &result)
dfs(root: root.right, level: level + 1, result: &result)
}

private func levelOrderDeuplicate(_ root: TreeNode?) -> [[Int]] {
var result = [[Int]]()
dfsDuplicate(root, level: 0, result: &result)
return result
}

private func dfsDuplicate(_ root: TreeNode?, level: Int, result: inout [[Int]]) {

guard let root = root else {
return
}

//logic
if (result.count == level) {
//means the array for a particular level is not present, so add it
result.append([])
}
//action
result[level].append(root.val)
dfsDuplicate(root.left, level: level + 1, result: &result)
dfsDuplicate(root.right, level: level + 1, result: &result)
}
}