Tag: swift

  • iOS, Find least common ancestor from 2 UIViews

    iOS, Find least common ancestor from 2 UIViews

    Find the least common ancestor.

    UIView is a tree data structure. Each node (UIView) can have multiple subviews.

    To find the least common ancestor, You can use either recursive calling or using a superview property.

    screenshot 2024 05 08 at 12.52.27e280afam

    UIView provide a useful functions and properties

    • subviews: [UIView]
    • superview: UIView?
    • func isDescendant(of: UIView) -> Bool

    Let’s create a View Trees.

     @discardableResult
        func insertView(_ at: UIView, tag: Int) -> UIView {
            let node = UIView()
            node.tag = tag
            at.addSubview(node)
            return node
        }
    
    func setupViewTree() {
            //insert level 1
            let tag0view = insertView(view, tag: 11)
            insertView(view, tag: 1)
            let tag2view = insertView(view, tag: 2)
            
            //insert level 2
            insertView(tag0view, tag: 3)
            let tag4view = insertView(tag0view, tag: 4)
            
            insertView(tag2view, tag: 5)
            let tag6view = insertView(tag2view, tag: 6)
            insertView(tag2view, tag: 7)
            
            //insert level 3
            insertView(tag4view, tag: 8)
            insertView(tag6view, tag: 9)
        }
    
    

    Helper function – Find a view with Tag

    func findView(_ at: UIView, tag: Int) -> UIView? {
            //Base case
            if at.tag == tag {
                return at
            }
            guard !at.subviews.isEmpty else {
                return nil
            }
            let subviews = at.subviews
            var targetView: UIView?
            for subview in subviews {
                if let view = findView(subview, tag: tag) {
                    targetView = view
                    break
                }
            }
            return targetView
        }

    Approach 1. Use Recursive Call

    If you want to see this logic step by step then visit my previous post.

    This approach is start from rootView. So we have to know what’s the root view.

    func findLeastCommonParentFromRootView(rootView: UIView, targetView1: UIView?, targetView2: UIView?) -> UIView? {
            guard let targetView1, let targetView2 else {
                return nil
            }
            return recursiveFindLeastCommonParentUsing(
                rootView: rootView,
                targetView1: targetView1,
                targetView2: targetView2
            )
        }
    
    
    private func recursiveFindLeastCommonParentUsing(
            rootView: UIView?,
            targetView1: UIView,
            targetView2: UIView
        ) -> UIView? {
            guard let rootView else {
                return nil
            }
            //Base case, prevent search targetView's childs
            if rootView == targetView1 || rootView == targetView2 {
                return rootView
            }
            let subviews = rootView.subviews
            var ancestors = [UIView]()
            for view in subviews {
                if let ancestorView = recursiveFindLeastCommonParentUsing(rootView: view, targetView1: targetView1, targetView2: targetView2) {
                    ancestors.append(ancestorView)
                }
            }
            if ancestors.contains(targetView1), ancestors.contains(targetView2) {
                return rootView
            }
            if ancestors.isEmpty {
                return nil
            }
            return ancestors.last
        }
    

    Approach 2. Use superview

    This approach is much simpler than approach 1. It uses a superview. Not calling a function recursively.

    private func findLeastCommonParentWithoutRootView(targetView1: UIView?, targetView2: UIView?) -> UIView? {
            guard targetView1 != nil, targetView2 != nil else {
                return nil
            }
            var target1 = targetView1
            var target2 = targetView2
            
            while target1?.superview != nil, target2?.superview != nil {
                let targetView1SuperView = target1?.superview
                var targetView2SuperView = target2?.superview
                while targetView2SuperView?.superview != nil {
                    if targetView1SuperView == targetView2SuperView {
                        return targetView2SuperView
                    }
                    targetView2SuperView = targetView2SuperView?.superview
                }
                target1 = target1?.superview
                target2 = target2?.superview
            }
            
            if target1?.subviews != nil {
                while target1?.superview != nil {
                    target1 = target1?.superview
                }
            }
            if target2?.subviews != nil {
                while target2?.superview != nil {
                    target2 = target2?.superview
                }
            }
            if target1 == target2 {
                return target1
            }
            else {
                return nil
            }
        }

    If each target view’s rootView is different then It will returns nil

    Approach 3. Use isDescendant

    func findLeastCommonParentUsingIsDescendantView(rootView: UIView, targetView1: UIView?, targetView2: UIView?) -> UIView? {
            guard let targetView1, let targetView2 else {
                return nil
            }
            var root: UIView = rootView
            var stack = [UIView]()
            //Base case
            guard targetView1.isDescendant(of: root), targetView2.isDescendant(of: root) else {
                return nil
            }
            while !root.subviews.isEmpty {
                for subview in root.subviews {
                    if targetView1 != subview, targetView1.isDescendant(of: subview), targetView2 != subview, targetView2.isDescendant(of: subview) {
                        stack.append(subview)
                    }
                }
                if let last = stack.last, root != last {
                    root = last
                }
                else {
                    break
                }
            }
            return stack.last ?? root
        }

    Test all 3 approaches

     override func viewDidLoad() {
            super.viewDidLoad()
            setupViewTree()
            let result = findLeastCommonParentFromRootView(
                rootView: view,
                targetView1: findView(view, tag: 5),
                targetView2: findView(view, tag: 9)
            )
            print("🟢 Approach 1 Least common parent: \(result?.tag)")
            
            let result2 = findLeastCommonParentWithoutRootView(
                targetView1: findView(view, tag: 5),
                targetView2: findView(view, tag: 9)
            )
            print("🟢 Approach 2  Least common parent: \(result2?.tag)")
            
            let result3 = findLeastCommonParentUsingIsDescendantView(
                rootView: view,
                targetView1: findView(view, tag: 5),
                targetView2: findView(view, tag: 9)
            )
            print("🟢 Approach 3  Least common parent: \(result3?.tag)")
        }
    screenshot 2024 05 08 at 1.28.38e280afam

  • iOS, Implement Least Recently Used Cache

    iOS, Implement Least Recently Used Cache

    ✍️ Note

    Some codes are sourced from Holczer Balazs’s Udemy Course (https://www.udemy.com/course/algorithms-and-data-structures/learn/lecture/9779302#content). This post is for personal notes where I summarize the original contents to grasp the key concepts

    Least Recently Used Cache

    We want to access the recently used items

    • For example URLs very fast so in O(1) time complexity and discard least recently used ones
    • Naive Approach: Use a single hash table and we can achieve put() and get() operations in O(1) but memory complexity is not favorable
    • Splay trees are working fine but again we store all the items in a tree-like structure
    • LRU caches use (usually) doubly linked lists to achieve this goal + we have to use hash tables as well to boost linked list!!

    https://www.udemy.com/course/algorithms-and-data-structures/learn/lecture/9779306#content

    screenshot 2024 05 01 at 1.15.55e280afpm
    public class Node {
        public var prev: Node?
        public var next: Node?
        public var id: String
        public var data: String
        
        public init(prev: Node? = nil, next: Node? = nil, id: String, data: String) {
            self.prev = prev
            self.next = next
            self.id = id
            self.data = data
        }
    }
    public class LRUCacheDoublyLinkedList {
        private var size: Int = 0
        let capacity: Int
        private var head: Node?
        private var tail: Node?
        
        
        private var dict: [String: Node] = [:]
        
        public init(capacity: Int) {
            self.capacity = capacity
        }
        
        //Update the node to be the head
        private func update(_ node: Node) {
            let prev = node.prev
            let next = node.next
            
            //It is a middle node in the list
            if prev != nil {
                prev?.next = next
            }
            else {
                //This case It is first node(head)
                head = next
            }
            //If it is not last node
            if next != nil {
                next?.prev = prev
            }
            else {
                //It is the last node (tail)
                tail = prev
            }
            add(node)
        }
        
        //get item with ID and move to the head because It used
        public func get(_ id: String) -> Node? {
            guard let node = dict[id] else {
                return nil
            }
            update(node)
            return node
        }
        
        //Always add node at Head
        private func add(_ node: Node) {
            node.next = head
            if head != nil {
                head?.prev = node
            }
            //Set new head node
            head = node
            
            //If there is 1 node in the list, it is the head as well as the tail
            if tail == nil {
                tail = node
            }
            dict[node.id] = node
        }
        
        private func removeTail() {
            let tailId = tail?.id
            var cachedTail: Node?
            if let tailId = tail?.id, let lastNode = dict[tailId] {
                cachedTail = lastNode
            }
            tail = tail?.prev
            if tail != nil {
                tail?.next = nil
            }
            
            if let tailId {
                dict.removeValue(forKey: tailId)
            }
            cachedTail = nil
        }
        
        public func put(id: String, data: String) {
            if let node = dict[id] {
                node.data = data
                update(node)
                return
            }
            
            //the data is not present in the cache so insert
            let newNode = Node(id: id, data: data)
            
            //Check cache size
            if size < capacity {
                size += 1
                //insert into the cache and set it to be the head node
                add(newNode)
            }
            else {
                //cache is full, remove the last item and insert new one
                removeTail()
                add(newNode)
            }
        }
        
        public func printAll() {
            var pointNode = head
            while pointNode != nil {
                if let data = pointNode?.data {
                    print("\(data)", terminator: pointNode?.next != nil ? " -> " : "")
                }
                pointNode = pointNode?.next
            }
            print("\n")
        }
    }
    
    screenshot 2024 04 26 at 2.35.33e280afpm
  • iOS, General Programming Problems

    iOS, General Programming Problems

    ✍️ Note

    Some codes are sourced from Loony Corn’s Udemy Course (https://www.udemy.com/user/janani-ravi-2/). This post is for personal notes where I summarize the original contents to grasp the key concepts

    General Programming Problems

    Often coding interviews involve problems which do not have complicated algorithms or data structures – These are straight programming problems

    They test your ability to work through details and get the edge cases right

    The bad thing about them is that they involve lots of cases which you need to work through – They tend to be frustrating

    The great thing about them is that if you are organised and systematic in your thought process it is reasonably straightforward to get them right

    These are problems you should nail. All they need is practice

    None of them require any detailed knowledge of standard algorithms

    We’ll solve 8 general programming problems here – They all use arrays or simple data structures which you create

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8462914#overview

    Example 1. Check whether a given string is a palindrome

    Palindromes are strings which read the same when read forwards or backwards

    You can reverse all the letters in a palindrome and get the original string

    Examples of palindromes are

    • MADAM, REFER, LONELY TYLENOL

    Note

    • the string can have spaces, ignore spaces in the palindrome check, the spaces can all be collapsed
    • The check is Case-Insenstive
    screenshot 2024 04 23 at 2.29.30e280afpm
    public func isPalindrome(_ input: String) -> Bool {
        var firstIndex = 0
        var lastIndex = input.count - 1
        let lowerCasedInput = Array(input.lowercased())
        print(lowerCasedInput)
        while firstIndex < lastIndex {
            var first: Character? = lowerCasedInput[firstIndex]
            var last: Character? = lowerCasedInput[lastIndex]
            
            while lowerCasedInput[firstIndex] == " " {
                firstIndex += 1
                first = lowerCasedInput[firstIndex]
            }
            
            while lowerCasedInput[lastIndex] == " " {
                lastIndex -= 1
                last = lowerCasedInput[lastIndex]
            }
            
            print("First char: \(first) Last char: \(last)")
            if first != last {
                return false
            }
            firstIndex += 1
            lastIndex -= 1
            
            print("First index: \(firstIndex)")
            print("Last index: \(lastIndex)")
        }
        return true
    }
    
    isPalindrome("MaIay a Ia m") //True
    isPalindrome("MADAM") //True
    isPalindrome("REFER") //True
    isPalindrome("LONELY TYLENOL") //True
    isPalindrome("LONELY TYLENOLF") //False
    
    screenshot 2024 04 23 at 3.20.50e280afpm

    Example 2. Find all points within a certain distance of another point

    Find points (Given X, Y coordinates) which are within a certain distance of another point. The distance and the central point is specified as an argument to the function which computes the points in range

    Example. All points within a distance 10 of (0,0) will include the point at (3, 4) but not include the point at (12, 13)

    Hint: If you are using an OO Programming language set up an entity which represents a point and contains methods within it to find the distance from another point.

    screenshot 2024 04 24 at 12.47.52e280afpm
    screenshot 2024 04 24 at 1.01.53e280afpm
    public struct Point {
        public let x: Double
        public let y: Double
        
        public init(x: Double, y: Double) {
            self.x = x
            self.y = y
        }
    }
    
    extension Point {
        public func getDistance(_ otherPoint: Point) -> Double {
            //Distance: sqrt(x^2 + y^2)
            return sqrt(pow(otherPoint.x - x, 2) + pow(otherPoint.y - y, 2))
        }
        
        public func isWithinDistance(_ otherPoint: Point, distance: Double) -> Bool {
            let distanceX = abs(x - otherPoint.x)
            let distanceY = abs(y - otherPoint.y)
            if distanceX > distance || distanceY > distance {
                return false
            }
            return getDistance(otherPoint) <= distance
        }
    }
    
    public func getPointsWithinDistance(points: [Point], center: Point, distance: Double) -> [Point] {
        var withinPoints = [Point]()
        for point in points {
            if point.isWithinDistance(center, distance: distance) {
                withinPoints.append(point)
            }
        }
        print("Points within \(distance) of point x: \(center.x) y: \(center.y)")
        for p in withinPoints {
            print("Point: x: \(p.x) y: \(p.y)")
        }
        return withinPoints
    }
    
    screenshot 2024 04 24 at 1.11.19e280afpm

    Example 3. Game of Life: Get the next generation of cell states

    A cell is a block in a matrix surrounded by neighbours. A cell can be in two states, alive or dead. A cell can change its state from one generation to another under certain circumstances

    Rule

    • A live cell with fewer than 2 live neighbours dies of loneliness
    • A dead cell with exactly 2 live neighbours comes alive
    • A live cell with greater than 2 live neighbours dies due to overcrowding

    Given a current generation of cells in a matrix, what does the next generation look like? Which cells are alive and which are dead? Write code to get the next generation of cells given the current generation

    Hint

    • Represent each generation as rows and columns in a 2D matrix. Live and Dead states can be represented by integers or boolean states
    screenshot 2024 04 24 at 1.33.22e280afpm
    func printMatrix(_ input: [[Int]]) {
        for row in 0..<input.count {
            print("|", terminator: "")
            for col in 0..<input[0].count {
                print("\(input[row][col])", terminator: "|")
            }
            print("\n")
        }
    }
    public func getNextGeneration(currentGeneration: [[Int]]) -> [[Int]] {
        var nextGeneration = currentGeneration
        let rowCount = currentGeneration.count
        let colCount = currentGeneration[0].count
        for row in 0..<rowCount {
            for col in 0..<colCount {
                let nextState = getNextState(
                    currentGeneration: currentGeneration,
                    row: row,
                    col: col
                )!
                nextGeneration[row][col] = nextState
            }
        }
        return nextGeneration
    }
    
    //0: Dead, 1: Alive
    public func getNextState(currentGeneration: [[Int]], row: Int, col: Int) -> Int? {
        //Edge case
        if currentGeneration.isEmpty {
            return nil
        }
        if row > currentGeneration.count {
            return nil
        }
        if col > currentGeneration[0].count {
            return nil
        }
        var states = [Int: Int]()
        let currentState = currentGeneration[row][col]
        let rowCount = currentGeneration.count
        let colCount = currentGeneration[0].count
        //Check 8 directions
        if col - 1 >= 0 {
            if let value = states[currentGeneration[row][col - 1]]{
                states[currentGeneration[row][col - 1]] = value + 1
            }
            else {
                states[currentGeneration[row][col - 1]] = 1
            }
        }
        if col - 1 >= 0, row - 1 >= 0 {
            if let value = states[currentGeneration[row - 1][col - 1]]{
                states[currentGeneration[row - 1][col - 1]] = value + 1
            }
            else {
                states[currentGeneration[row - 1][col - 1]] = 1
            }
        }
        if col - 1 >= 0, row + 1 < rowCount {
            if let value = states[currentGeneration[row + 1][col - 1]]{
                states[currentGeneration[row + 1][col - 1]] = value + 1
            }
            else {
                states[currentGeneration[row + 1][col - 1]] = 1
            }
        }
        if col + 1 < colCount {
            if let value = states[currentGeneration[row][col + 1]]{
                states[currentGeneration[row][col + 1]] = value + 1
            }
            else {
                states[currentGeneration[row][col + 1]] = 1
            }
        }
        if col + 1 < colCount, row - 1 >= 0 {
            if let value = states[currentGeneration[row - 1][col + 1]]{
                states[currentGeneration[row - 1][col + 1]] = value + 1
            }
            else {
                states[currentGeneration[row - 1][col + 1]] = 1
            }
        }
        if col + 1 < colCount, row + 1 < rowCount {
            if let value = states[currentGeneration[row + 1][col + 1]]{
                states[currentGeneration[row + 1][col + 1]] = value + 1
            }
            else {
                states[currentGeneration[row + 1][col + 1]] = 1
            }
        }
        if row + 1 < rowCount {
            if let value = states[currentGeneration[row + 1][col]]{
                states[currentGeneration[row + 1][col]] = value + 1
            }
            else {
                states[currentGeneration[row + 1][col]] = 1
            }
        }
        if row - 1 >= 0 {
            if let value = states[currentGeneration[row - 1][col]]{
                states[currentGeneration[row - 1][col]] = value + 1
            }
            else {
                states[currentGeneration[row - 1][col]] = 1
            }
        }
        
        if currentState == 0 {
            //A dead cell with exactly 2 live neighbours comes alive
            let livedCount = states[1] ?? 0
            return livedCount == 2 ? 1 : 0
        }
        else {
            let livedCount = states[1] ?? 0
            //A live cell with fewer than 2 live neighbours dies of loneliness
            //A live cell with greater than 2 live neighbours dies due to overcrowding
            if livedCount < 2 || livedCount >= 2 {
                return 0
            }
            return currentState
        }
    }
    screenshot 2024 04 24 at 2.09.45e280afpm

    Example 4. Break A document into chunks

    A document is stored in the cloud and you need to send the text of the document to the client which renders it on the screen (Think Google docs)

    You do not want to send the entire document to the client at one go, you want to send it chunk by chunk. A chunk can be created subject to certain constraints

    Rule

    • A chunk can be 5000 or fewer characters in length (This rule is relaxed only under one condition see below)
    • A chunk should contain only complete paragraphs – This is a hard and fast rule
    • A paragraph is represented by the ‘:’ Character in the document
    • List of chunks should be in the order in which they appear in the document (Do not set them up out of order)
    • If you encounter a paragraph > 5000 characters that should be in a separate chunk by itself
    • Get all chunks as close to 5000 characters as possible, subject to the constraints above

    Given a string document return a list of chunks which some other system can use to send to the client

    Hint: public func chunkify(doc: String) -> [String]

    screenshot 2024 04 24 at 2.40.57e280afpm
    public func chunkify(doc: String, chunkSize: Int) -> [String] {
        guard !doc.isEmpty else {
            return []
        }
        var chunks = [String]()
        let paragraphs = doc.split(separator: ":")
        var start = 0
        let total = paragraphs.count
        
        while start < total {
            var chunk = paragraphs[start] + ":"
            if chunk.count <= chunkSize {
                var next = start + 1
                while next < total, paragraphs[next].count + 1 <= chunkSize - chunk.count {
                    chunk += (paragraphs[next] + ":")
                    next += 1
                }
                chunks.append(String(chunk))
                start = next
            }
            else {
                chunks.append(String(chunk))
                start += 1
            }
        }
        return chunks
    }
    screenshot 2024 04 24 at 3.06.53e280afpm

    Example 5. Run Length Encoding and Decoding

    Write code which encodes a string using Run-Length encoding and decodes a string encoded using Run-Length encoding

    Example

    • ABBCCC -> 1A2B3C
    • AABBBCCCC -> 2A3B4C
    • 1D2E1F -> DEEF

    Constraints

    Assume only letters are present in the string to be encode, no numbers

    Remember to handle the case where we can have > 9 of the same characters in a row

    Run-Length Encoding Code

    public func runLengthEncoding(_ input: String) -> String {
        var result = ""
        var start = 0
        let arrayInput = Array(input)
        while start < arrayInput.count {
            var count = 1
            let currentCharacter = arrayInput[start]
            var next = start + 1
            while next < arrayInput.count, arrayInput[next] == currentCharacter {
                count += 1
                next += 1
            }
            result += "\(count)\(currentCharacter)"
            start = next
        }
        return result
    }
    screenshot 2024 04 24 at 3.42.54e280afpm

    Run-Length Decoding Code

    public func runLengthDecoding(_ input: String) -> String {
        var result = ""
        var start = 0
        let arrayInput = Array(input)
        
        //Assumed that first character start with number
        while start < arrayInput.count {
            //Step 1. Get number
            var numberStr = String(arrayInput[start])
            //Edge case. Larger than 9
            var next = start + 1
            while next < arrayInput.count, Int(String(arrayInput[next])) != nil {
                numberStr += String(arrayInput[next])
                print(numberStr)
                next += 1
            }
            let repeatedCount = Int(numberStr) ?? 1
            
            //Step 2. Generate repeated characters
            result += String(repeating: arrayInput[next], count: repeatedCount)
            
            //Step 3. Important! Pointing to next number
            start = next + 1
        }
        return result
    }
    screenshot 2024 04 24 at 4.01.42e280afpm

    Example 6. Add two numbers represented by their digits

    Given two numbers where the individual digits in the numbers are in an array or a list add them to get the final result in the same list or array form

    Example using Arrays: [1, 2] represents the number 12. Note that the most significant digit in the 0th index of the array, with the least significant digit at the last position

    Adding [1, 2] and [2, 3] should give the result [3, 5]

    Requirements

    • Don’t convert the number format to a real number to add them. Add them digit by digit
    • Remember to consider the carry over per digit if there is one!
    screenshot 2024 04 24 at 4.25.26e280afpm
    public func add(a: [Int], b: [Int]) -> [Int] {
        var result = [Int]()
        let maxLength = max(a.count, b.count)
        var carry = 0
        var currentIndexA = a.count == maxLength ? maxLength - 1 : a.count - 1
        var currentIndexB = b.count == maxLength ? maxLength - 1 : b.count - 1
        
        while currentIndexA >= 0, currentIndexB >= 0 {
            var sum = a[currentIndexA] + b[currentIndexB] + carry
            if sum > 9 {
                carry = sum / 10
                sum = sum % 10
            }
            else {
                carry = 0
            }
            result.insert(sum, at: 0)
            currentIndexA -= 1
            currentIndexB -= 1
        }
        if currentIndexA >= 0 {
            while currentIndexA >= 0 {
                var sum = a[currentIndexA] + carry
                if sum > 9 {
                    carry = sum / 10
                    sum = sum % 10
                }
                else {
                    carry = 0
                }
                result.insert(sum, at: 0)
                currentIndexA -= 1
            }
        }
        
        if currentIndexB >= 0 {
            while currentIndexB >= 0 {
                var sum = a[currentIndexB] + carry
                if sum > 9 {
                    carry = sum / 10
                    sum = sum % 10
                }
                else {
                    carry = 0
                }
                result.insert(sum, at: 0)
                currentIndexB -= 1
            }
        }
        
        
        if carry != 0 {
            result.insert(carry, at: 0)
        }
        
        return result
    }
    
    screenshot 2024 04 24 at 8.36.44e280afpm

    Example 7. Increment number by 1

    Suppose that you invent your own numeral system (which is neither decimal, binary nor any of the common ones). You specify the digits and the order of the digits in that numeral system.

    Given the digits and the order of digits used in that system and a number, write a function to increment that number by 1 and return the result

    screenshot 2024 04 24 at 9.06.32e280afpm

    Solution 1

    public func incrementByOne(input: String) -> String? {
        var result = [String]()
        let numeralSystem = ["A": 0, "B": 1, "C": 2, "D": 3]
        var input = Array(input).compactMap { "\($0)"}
        
        var validCount = 0
        
        for char in input {
            if numeralSystem.keys.contains(char) {
                validCount += 1
            }
        }
        if validCount != input.count {
            return nil
        }
        
        var currentIndex = input.count - 1
        
        var carry = 0
        while currentIndex >= 0 {
            var sum = numeralSystem[input[currentIndex]]! + carry
            if currentIndex == input.count - 1 {
                sum += 1
            }
            if sum > 3 {
                carry = sum / 4
                sum = sum % 4
            }
            else {
                carry = 0
            }
            if let key = numeralSystem.first (where: { $0.value == sum })?.key {
                result.insert(key, at: 0)
            }
            currentIndex -= 1
        }
        
        if carry != 0 {
            if let key = numeralSystem.first (where: { $0.value == carry })?.key {
                result.insert(key, at: 0)
            }
        }
        return result.joined()
    }
    
    screenshot 2024 04 24 at 9.24.19e280afpm

    Solution 2

    public func incrementByOne2(input: String) -> String? {
        let numeralSystem = ["A", "B", "C", "D"]
        var input = Array(input).compactMap { "\($0)" }
        
        var validCount = 0
        for char in input {
            if numeralSystem.contains(char) {
                validCount += 1
            }
        }
        if validCount != input.count {
            return nil
        }
        
        var currentIndex = input.count - 1
        var isCompleted = false
        while !isCompleted, currentIndex >= 0 {
            let currentDigit = input[currentIndex]
            let indexOfCurrentDigit = numeralSystem.firstIndex(of: currentDigit)
            
            //Get the next digit on increment, this will wrap around to the first digit which is why we use the modulo operator
            let indexOfNextDigit = (indexOfCurrentDigit! + 1) % numeralSystem.count
            
            //Update digit
            input[currentIndex] = numeralSystem[indexOfNextDigit]
            
            if indexOfNextDigit != 0 {
                isCompleted = true
            }
            
            //If we're at the most significant digit and that wrapped around we add a new digit to the incremented number like going from 9 to 10
            if currentIndex == 0, indexOfNextDigit == 0 {
                input.insert(numeralSystem[0], at: 0)
            }
            currentIndex -= 1
        }
        return input.joined()
    }
    
    screenshot 2024 04 24 at 9.36.42e280afpm

    Example 8. Sudoku

    screenshot 2024 03 23 at 10.12.33e280afpm
    screenshot 2024 03 23 at 11.58.23e280afpm

    Basic rules

    • Each rows and columns should have one value (1-9) -> Should not have duplicated value
    • Board size is 9×9
    • Block size is 3×3, Total 9 blocks
    • In a Block, Should have one value (1-9) -> Should not have duplicated value

    Exercise 1. isValidRowsAndCols

    This function is returns boolean. If all the row and column’s values are valid then It returns true. otherwise returns false.

    screenshot 2024 03 24 at 12.13.14e280afam
    var sudoku: [[Int]] = [
        [8, 0, 0,   4, 0, 6,   0, 0, 7],
        [0, 0, 0,   0, 0, 0,   4, 0, 0],
        [0, 1, 0,   0, 0, 0,   6, 5, 0],
        
        [5, 0, 9,   0, 3, 0,   7, 8, 0],
        [0, 0, 0,   0, 7, 0,   0, 0, 0],
        [0, 4, 8,   0, 2, 0,   1, 0, 3],
        
        [0, 5, 2,   0, 0, 0,   0, 0, 0],
        [0, 0, 1,   0, 0, 0,   0, 0, 0],
        [3, 0, 0,   9, 0, 2,   0, 0, 5]
    ]
    //Helper
    func printBoard(_ input: [[Int]]) {
        for row in 0..<9 {
            print("|", terminator: "")
            for col in 0..<9 {
                print("\(input[row][col])", terminator: "|")
            }
            print("\n")
        }
    }
    
    func isValidRowsAndCols(_ board: [[Int]]) -> Bool {
        var rowSet: [Int: Set<Int>] = [:]
        var colSet: [Int: Set<Int>] = [:]
        
        //Index starts from 0 to 8
        for i in 0...8 {
            rowSet[i] = Set()
            colSet[i] = Set()
        }
        
        for row in 0...8 {
            for col in 0...8 {
                var cellValue = board[row][col]
                //No value has assigned
                if cellValue == 0 {
                    continue
                }
                
                if cellValue < 0 || cellValue > 9 {
                    return false
                }
                //cellValue already seen
                if rowSet[row]?.contains(cellValue) == true {
                    print("💥 Invalid: \(row):\(col) -> value: \(cellValue)")
                    return false
                }
                if colSet[col]?.contains(cellValue) == true {
                    print("💥 Invalid: \(row):\(col) -> value: \(cellValue)")
                    return false
                }
                
                //Add current cell value to the row or column set
                rowSet[row]?.insert(cellValue)
                colSet[col]?.insert(cellValue)
            }
        }
        return true
    }
    
    isValidRowsAndCols(sudoku) //It returns true
    

    Exercise 2. IsValidBlock -> Bool

    screenshot 2024 03 23 at 11.58.23e280afpm

    Each block should have values between 1-9. And each cell should not have a duplicate value in a block.

    func isValidBlock(_ board: [[Int]]) -> Bool {
        //Set associated with every block
        var blockSet: [Int: Set<Int>] = [:]
        for i in 0..<9 {
            blockSet[i] = Set()
        }
        
        //Row block is 3
        for rowBlock in 0...2 {
            for colBlock in 0...2 {
                //Check 3x3 Block Cell
                for miniRow in 0...2 {
                    for miniCol in 0...2 {
                        let row = rowBlock * 3 + miniRow
                        let col = colBlock * 3 + miniCol
                        let cellValue = sudoku[row][col]
                        
                        if cellValue == 0 {
                            continue
                        }
                        if cellValue < 0 || cellValue > 9 {
                            return false
                        }
                        /* Total 9 block
                         row0: 0  1  2
                         row1: 3  4  5
                         row2: 6  7  8
                         */
                        let blockNumber = rowBlock * 3 + colBlock
                        
                        if blockSet[blockNumber]?.contains(cellValue) == true {
                            return false
                        }
                        blockSet[blockNumber]?.insert(cellValue)
                    }
                }
            }
        }
        return true
    }
    isValidBlock(sudoku) // It returns true
    
    screenshot 2024 03 24 at 12.16.41e280afam

    Exercise 3. isValidSudoku -> Bool

    This function checks all the cells in board and checks the sudoku rule.

    func isValid(_ board: [[Int]]) -> Bool {
        if !isValidRowsAndCols(board) {
            return false
        }
        if !isValidBlock(board) {
            return false
        }
        return true
    }

    It is very simple. It calls two functions we made. If all the cell’s value are valid then return true.

    Exercise 4. solveSudoku

    This function fill the cell’s value and returns solution.

    sudo

    https://www.sudokuonline.io

    As you can see when you select a cell (yellow), You need to check blue areas.

    Step 1. Create a Sudoku Board

    Generate Sudoku Board

    func generateSudoku(board: inout [[Int]]) {
        //Has a 9 mini blocks. diagonal direction
        //3 means jumping to next mini block, It loops 3 blocks and fill the numbers
        for i in stride(from: 0, through: 8, by: 3) {
            var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9].shuffled()
            for miniRow in 0...2 {
                for miniCol in 0...2 {
                    board[miniRow][miniCol] = numbers.removeFirst()
                }
            }
        }
        
        
        //Step 2. solve function to fill the all numbers
        _ = solve(board: &board)
        
        var numberOfEmptyCell = 50
        
        while numberOfEmptyCell > 0 {
            var random = [0, 1, 2, 3, 4, 5, 6, 7, 8].shuffled()
        
            board[random.first!][random.last!] = 0
            numberOfEmptyCell -= 1
        }
        //TODO: Step 3. set empty cells It depends on difficulty
    }
    screenshot 2024 04 21 at 8.55.59e280afpm

    Helper functions

    import Foundation
    //https://github.com/ShawnBaek/Table
    import Table
    
    //Check If Sudoku board has empty cell or not
    func isFilledAllNumbers(board: [[Int]]) -> Bool {
        var numberOfEmptyCell = 0
        for row in 0...8 {
            for col in 0...8 {
                if board[row][col] == 0 {
                    numberOfEmptyCell += 1
                }
            }
        }
        return numberOfEmptyCell == 0
    }
    
    //Get emptyCell's position
    func emptyCell(board: [[Int]]) -> (row: Int, col: Int)? {
        for row in 0...8 {
            for col in 0...8 {
                //Empty cell found
                if board[row][col] == 0 {
                    return (row: row, col: col)
                }
            }
        }
        return nil
    }
    
    func canPlace(_ number: Int, row: Int, col: Int, board: [[Int]]) -> Bool {
        //Sudoku rule
        
        //Rule 1. Check unique number in a row
        for r in 0...8 {
            if board[r][col] == number {
                return false
            }
        }
        
        //Rule 2. Check unique number in a col
        for c in 0...8 {
            if board[row][c] == number {
                return false
            }
        }
        
        //Rule 3. Check unique number in a mini block -> total 9
        //each block's start position will be 0,0  3,3, 6,6, and so on
        
        //If row is between 3-5, 3...5 / 3 = 1, 1 * 3 = 3
        let miniBlockStartRow = (row / 3) * 3
        let miniBlockStartCol = (col / 3) * 3
        
        for miniRow in 0...2 {
            for miniCol in 0...2 {
                let boardRow = miniBlockStartRow + miniRow
                let boardCol = miniBlockStartCol + miniCol
                if board[boardRow][boardCol] == number {
                    return false
                }
            }
        }
        return true
    }

    Sudoku Board solve function

    • Step 1. Check isFilledAllNumber or not.
      • It assumed that generated board is a valid sudoku board. All we need to do is fill the correct number on empty cell
    • Step 2. Recursively calling a function – Get empty cell and fill the correct number
    //It is a recursive function
    func solve(board: inout [[Int]]) -> Bool {
        //Base case
        //If this board filled all numbers? -> yes then return true
        //func isFilledAllNumbers -> Bool
        //Game done
        if isFilledAllNumbers(board: board) {
            return true
        }
        
        //Step 1. Get the emptyCell and finding a number to place
        //one more function to get emptyCell
        if let emptyCellPosition = emptyCell(board: board) {
            //Can place 1-9 numbers
            for number in 1...9 {
                //Check can place a number at this position
                if canPlace(number, row: emptyCellPosition.row, col: emptyCellPosition.col, board: board) {
                    //Place a number
                    board[emptyCellPosition.row][emptyCellPosition.col] = number
                    //Recursive Case
                    if solve(board: &board) {
                        return true
                    }
                    //Reset to emptyCell
                    board[emptyCellPosition.row][emptyCellPosition.col] = 0
                }
            }
        }
        return false
    }

    Check Validation

    func isValidRowsAndCols(_ board: [[Int]]) -> Bool {
        var rowSet: [Int: Set<Int>] = [:]
        var colSet: [Int: Set<Int>] = [:]
        
        //Index starts from 0 to 8
        for i in 0...8 {
            rowSet[i] = Set()
            colSet[i] = Set()
        }
        
        for row in 0...8 {
            for col in 0...8 {
                let cellValue = board[row][col]
                //No value has assigned
                if cellValue == 0 {
                    continue
                }
                
                if cellValue < 0 || cellValue > 9 {
                    return false
                }
                //cellValue already seen
                if rowSet[row]?.contains(cellValue) == true {
                    return false
                }
                if colSet[col]?.contains(cellValue) == true {
                    return false
                }
                
                //Add current cell value to the row or column set
                rowSet[row]?.insert(cellValue)
                colSet[col]?.insert(cellValue)
            }
        }
        return true
    }
    
    
    
    func isValidBlock(_ board: [[Int]]) -> Bool {
        //Set associated with every block
        var blockSet: [Int: Set<Int>] = [:]
        for i in 0..<9 {
            blockSet[i] = Set()
        }
        
        var test = 0
        //Row block is 3
        for rowBlock in 0...2 {
            for colBlock in 0...2 {
                print("Start Block: \(test)")
                //Check 3x3 Block Cell
                for miniRow in 0...2 {
                    for miniCol in 0...2 {
                        let row = rowBlock * 3 + miniRow
                        let col = colBlock * 3 + miniCol
                        let cellValue = board[row][col]
                        
                        if cellValue == 0 {
                            continue
                        }
                        if cellValue < 0 || cellValue > 9 {
                            return false
                        }
                        /* Total 9 block
                         row0: 0  1  2
                         row1: 3  4  5
                         row2: 6  7  8
                         */
                        let blockNumber = rowBlock * 3 + colBlock
                        
                        if blockSet[blockNumber]?.contains(cellValue) == true {
                            return false
                        }
                        blockSet[blockNumber]?.insert(cellValue)
                    }
                }
                
                test += 1
                
            }
            
        }
        return true
    }
    
    func isValid(_ board: [[Int]]) -> Bool {
        if !isValidRowsAndCols(board) {
            return false
        }
        if !isValidBlock(board) {
            return false
        }
        return true
    }
    screenshot 2024 04 21 at 8.57.18e280afpm 5
  • iOS, Combine Framework

    iOS, Combine Framework

    ✍️ Note

    Some codes and contents are sourced from Apple, WWDC and BigMountStudio. This post is for personal notes where I summarize the original contents to grasp the key concepts (🎨 some images I draw it)

    Overview

    screenshot 2024 04 17 at 9.28.49e280afpm

    The Combine framework provides a declarative approach for how your app processes events. Rather than potentially implementing multiple delegate callbacks or completion handler closures, you can create a single processing chain for a given event source. Each part of the chain is a Combine operator that performs a distinct action on the elements received from the previous step.

    Apple

    screenshot 2024 04 17 at 2.12.51e280afpm

    Publisher is struct

    associatedtype failure error
    screenshot 2024 04 17 at 12.20.47e280afpm
    screenshot 2024 04 17 at 10.07.54e280afpm

    Array can publish values.

    I used delay, between publisher and subscriber. It controlling the timing like debounce and throttle. Above the example, It prints 1, 2, 3, 4 after 3 seconds.

    Subscriber

    It has two main feature.

    • receive
    • assign
    associatedtype failure erron
    screenshot 2024 04 17 at 12.21.58e280afpm

    Operators

    It adopts publisher and send results to subscriber

    • Upstream: Publisher – Subscribes to Publisher
    • Downstream: Subscriber – Sends results to a Subscriber
    extension publishers
    screenshot 2024 04 17 at 12.32.16e280afpm

    Publisher’s map function. It’s upstream is self

    notification center
    screenshot 2024 04 17 at 12.31.04e280afpm
    screenshot 2024 04 17 at 12.32.49e280afpm

    Subjects

    A publisher that exposes a method for outside callers to publish elements.

    A subject is a publisher that you can use to ”inject” values into a stream, by calling its send(_:) method. This can be useful for adapting existing imperative code to the Combine model.

    • CurrentValueSubject
    • PassthroughSubject
    behave like both publisher and subscriber

    PassthroughSubject

    A subject that broadcasts elements to downstream subscribers.

    Unlike CurrentValueSubject, a PassthroughSubject doesn’t have an initial value or a buffer of the most recently-published element. A PassthroughSubject drops values if there are no subscribers, or its current demand is zero.

    screenshot 2024 04 17 at 11.50.03e280afpm

    CurrentValueSubject

    A subject that wraps a single value and publishes a new element whenever the value changes.

    Unlike PassthroughSubjectCurrentValueSubject maintains a buffer of the most recently published element.

    Calling send(_:) on a CurrentValueSubject also updates the current value, making it equivalent to updating the value directly.

    screenshot 2024 04 17 at 11.50.42e280afpm

    Map

    Transforms all elements from the upstream publisher with a provided closure.

    func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>

    Parameters

    transform

    A closure that takes one element as its parameter and returns a new element.

    Return Value

    A publisher that uses the provided closure to map elements from the upstream publisher to new elements that it then publishes.

    FlatMap

    Transforms all elements from an upstream publisher into a new publisher up to a maximum number of publishers you specify.

    func flatMap<T, P>( maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Self.Output) -> P ) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure

    Parameters

    maxPublishers

    Specifies the maximum number of concurrent publisher subscriptions, or unlimited if unspecified.transform

    A closure that takes an element as a parameter and returns a publisher that produces elements of that type.

    Return Value

    A publisher that transforms elements from an upstream publisher into a publisher of that element’s type.

    Combine‘s flatMap(maxPublishers:_:) operator performs a similar function to the flatMap(_:) operator in the Swift standard library, but turns the elements from one kind of publisher into a new publisher that is sent to subscribers. Use flatMap(maxPublishers:_:) when you want to create a new series of events for downstream subscribers based on the received value. The closure creates the new Publisher based on the received value. The new Publisher can emit more than one event, and successful completion of the new Publisher does not complete the overall stream. Failure of the new Publisher causes the overall stream to fail.

    In the example below, a PassthroughSubject publishes WeatherStation elements. The flatMap(maxPublishers:_:) receives each element, creates a URL from it, and produces a new URLSession.DataTaskPublisher, which will publish the data loaded from that URL.

    public struct WeatherStation {
        public let stationID: String
    }
    
    
    var weatherPublisher = PassthroughSubject<WeatherStation, URLError>()
    
    
    cancellable = weatherPublisher.flatMap { station -> URLSession.DataTaskPublisher in
        let url = URL(string:"https://weatherapi.example.com/stations/\(station.stationID)/observations/latest")!
        return URLSession.shared.dataTaskPublisher(for: url)
    }
    .sink(
        receiveCompletion: { completion in
            // Handle publisher completion (normal or error).
        },
        receiveValue: {
            // Process the received data.
        }
     )
    
    
    weatherPublisher.send(WeatherStation(stationID: "KSFO")) // San Francisco, CA
    weatherPublisher.send(WeatherStation(stationID: "EGLC")) // London, UK
    weatherPublisher.send(WeatherStation(stationID: "ZBBB")) // Beijing, CN
    screenshot 2024 04 17 at 2.01.05e280afpm
    flat map
    screenshot 2024 04 17 at 2.03.09e280afpm
    screenshot 2024 04 17 at 2.03.14e280afpm
    screenshot 2024 04 17 at 2.04.05e280afpm 1
    screenshot 2024 04 17 at 2.05.27e280afpm 1

    Future – Publisher

    A publisher that eventually produces a single value and then finishes or fails.

    Use a future to perform some work and then asynchronously publish a single element. You initialize the future with a closure that takes a Future.Promise; the closure calls the promise with a Result that indicates either success or failure. In the success case, the future’s downstream subscriber receives the element prior to the publishing stream finishing normally. If the result is an error, publishing terminates with that error.

    screenshot 2024 04 18 at 12.04.24e280afam

    Above the example, when you call a function It create a Future publisher. As you can see Future has finished after send a value (200)

    Apple’s example

    func generateAsyncRandomNumberFromFuture() -> Future <Int, Never> {
        return Future() { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                let number = Int.random(in: 1...10)
                promise(Result.success(number))
            }
        }
    }
    
    cancellable = generateAsyncRandomNumberFromFuture()
        .sink { number in print("Got random number \(number).") }
    
    //async-await syntax
    let number = await generateAsyncRandomNumberFromFuture().value
    print("Got random number \(number).")
    
    //Alternative to Futures
    func generateAsyncRandomNumberFromContinuation() async -> Int {
        return await withCheckedContinuation { continuation in
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                let number = Int.random(in: 1...10)
                continuation.resume(returning: number)
            }
        }
    }
    
    let asyncRandom = await generateAsyncRandomNumberFromContinuation()
    

    Demand – Publisher

    A publisher that awaits subscription before running the supplied closure to create a publisher for the new subscriber.

    screenshot 2024 04 18 at 12.19.08e280afam

    Unlike Future Publisher, It deferred publisher will not execute immediately when It is created.
    It will execute every time a subscriber is attached.

    You can use it for many of Apple’s Kits where you need to get information from a device, or ask the user for permissions to access something, like photos, or other private or sensitive information

    https://www.bigmountainstudio.com

    screenshot 2024 04 18 at 12.27.12e280afam

    Throttle – Publisher method

    Publishes either the most-recent or first element published by the upstream publisher in the specified time interval.

    interval

    The interval at which to find and emit either the most recent or the first element, expressed in the time system of the scheduler.

    When you set latest true, It returns the latest value. Otherwise It returns a first value.

    Share

    Shares the output of an upstream publisher with multiple subscribers.

    Publishers.Share is effectively a combination of the Publishers.Multicast and PassthroughSubject publishers, with an implicit autoconnect().

    The following example uses a sequence publisher as a counter to publish three random numbers, generated by a map(_:) operator. It uses a share() operator to share the same random number to each of two subscribers. This example uses a delay(for:tolerance:scheduler:options:) operator only to prevent the first subscriber from exhausting the sequence publisher immediately; an asynchronous publisher wouldn’t need this.

    Without the share() operator, stream 1 receives three random values, followed by stream 2 receiving three different random values.

    Also note that Publishers.Share is a class rather than a structure like most other publishers. This means you can use this operator to create a publisher instance that uses reference semantics.

    Example 1. Without Share – received values are different

    screenshot 2024 04 18 at 12.41.02e280afam

    Example 2. With share() – received values are the same

    screenshot 2024 04 18 at 12.41.38e280afam

    Connect / AutoConnect – Use for ConnectablePublisher

    Sometimes, you want to configure a publisher before it starts producing elements, such as when a publisher has properties that affect its behavior. But commonly used subscribers like sink(receiveValue:) demand unlimited elements immediately, which might prevent you from setting up the publisher the way you like. A publisher that produces values before you’re ready for them can also be a problem when the publisher has two or more subscribers. This multi-subscriber scenario creates a race condition: the publisher can send elements to the first subscriber before the second even exists.

    Consider the scenario in the following figure. You create a URLSession.DataTaskPublisher and attach a sink subscriber to it (Subscriber 1) which causes the data task to start fetching the URL’s data. At some later point, you attach a second subscriber (Subscriber 2). If the data task completes its download before the second subscriber attaches, the second subscriber misses the data and only sees the completion.

    screenshot 2024 04 18 at 12.35.50e280afam
    Hold Publishing by Using a Connectable Publisher

    To prevent a publisher from sending elements before you’re ready, Combine provides the ConnectablePublisher protocol. A connectable publisher produces no elements until you call its connect() method. Even if it’s ready to produce elements and has unsatisfied demand, a connectable publisher doesn’t deliver any elements to subscribers until you explicitly call connect().

    The following figure shows the URLSession.DataTaskPublisher scenario from above, but with a ConnectablePublisher ahead of the subscribers. By waiting to call connect() until both subscribers attach, the data task doesn’t start downloading until then. This eliminates the race condition and guarantees both subscribers can receive the data.

    screenshot 2024 04 18 at 12.36.01e280afam

    To use a ConnectablePublisher in your own Combine code, use the makeConnectable() operator to wrap an existing publisher with a Publishers.MakeConnectable instance. The following code shows how makeConnectable() fixes the data task publisher race condition described above. Typically, attaching a sink — identified here by the AnyCancellable it returns, cancellable1 — would cause the data task to start immediately. In this scenario, the second sink, identified as cancellable2, doesn’t attach until one second later, and the data task publisher might complete before the second sink attaches. Instead, explicitly using a ConnectablePublisher causes the data task to start only after the app calls connect(), which it does after a two-second delay.

    let url = URL(string: "https://example.com/")!
    let connectable = URLSession.shared
        .dataTaskPublisher(for: url)
        .map() { $0.data }
        .catch() { _ in Just(Data() )}
        .share()
        .makeConnectable()
    
    
    cancellable1 = connectable
        .sink(receiveCompletion: { print("Received completion 1: \($0).") },
              receiveValue: { print("Received data 1: \($0.count) bytes.") })
    
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.cancellable2 = connectable
            .sink(receiveCompletion: { print("Received completion 2: \($0).") },
                  receiveValue: { print("Received data 2: \($0.count) bytes.") })
    }
    
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        self.connection = connectable.connect()
    }

    Use the Autoconnect Operator If You Don’t Need to Explicitly Connect

    Some Combine publishers already implement ConnectablePublisher, such as Publishers.Multicast and Timer.TimerPublisher. Using these publishers can cause the opposite problem: having to explicitly connect() could be burdensome if you don’t need to configure the publisher or attach multiple subscribers.

    For cases like these, ConnectablePublisher provides the autoconnect() operator. This operator immediately calls connect() when a Subscriber attaches to the publisher with the subscribe(_:) method.

    The following example uses autoconnect(), so a subscriber immediately receives elements from a once-a-second Timer.TimerPublisher. Without autoconnect(), the example would need to explicitly start the timer publisher by calling connect() at some point.

    let cancellable = Timer.publish(every: 1, on: .main, in: .default)
        .autoconnect()
        .sink() { date in
            print ("Date now: \(date)")
         }
  • iOS, Graph Data Structure

    iOS, Graph Data Structure

    ✍️ Note

    Some codes and contents are sourced from Udemy. This post is for personal notes where I summarize the original contents to grasp the key concepts (🎨 some images I draw it)

    Graph

    A graph is used to represent relationships between entities

    Graphs are used to represent information in many many real world applications

    Graphs are also favorite interview questions – They can start from simple concepts and get arbitrarily complex

    Graph theory is a complex field of study by itself – There are many algorithms to optimize different problems represented using graphs

    First principle of graph can solve most of the graph problems

    Vertex

    • The entities can be anything – Graphs find applications in variety of ways in the real word

    Edge

    • These relationships can be arbitrarily complicated and of a variety of different types

    Real word Example

    • LinkedIn (Professional Graph)
      • Entities: People
      • Edge: Professional relationships – people work together
    • Facebook (Social Graph)
      • Entities: People
      • Edge: Personal relationships – People are friends
    • GoogleMaps (Locations)
      • Entities: Locations
      • Edge: A way to get from one location to another – Roads, Rail, Air
        • Each of these can be further thought of in terms of specific means of transport
        • Bus, Car, Taxi, Individual trains, Airlines
    • at&t (Phone network)
      • Entities: Old fashioned phones – landlines
      • Edge: A network to carry voice from one instrument to another
    • cisco / google and more (Internet)
      • Entities: Computers across the world
      • Edge: A way to send information or data from one computer to another
        • This can information can be routed wirelessly or over wires

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474120#content

    What is a graph?

    A graph is a set of vertices and edges (V, E)

    • A very simple graph is one vertex
    • Two vertices and a single edge is also a valid graph

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8484198#content

    Undirected Graph

    screenshot 2024 04 05 at 10.52.53e280afpm

    Undirected Graph

    screenshot 2024 04 07 at 1.37.49e280afpm
    screenshot 2024 04 07 at 1.37.13e280afpm

    Unconnected Graph

    screenshot 2024 04 07 at 1.42.20e280afpm

    Directed Graph

    screenshot 2024 04 07 at 1.47.46e280afpm

    Directed Acyclic Graph

    screenshot 2024 04 07 at 1.50.03e280afpm

    Graph Representation

    A way to model a vertex which may hold some information

    A way to model directed or undirected edges

    There are 3 standard ways that graphs can be represented

    • Adjacency Matrix
    • Adjacency List
    • Adjacency Set

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474130#content

    Graph Interface

    enum GraphType {
        case directed
        case undirected
    }
    
    protocol Graph {
        func addEdge(src: Int, dst: Int)
    //Helper to get the adjacent vertices for any vertex - A method which is required for all algorithms involving graphs
        func getAdjacentVertices(vertext: Int) -> [Int]?
    }

    Adjacency Matrix

    Use A Matrix with Rows and Columns – It just a Table

    The row labels and the column labels represent the vertices

    Each cell represents the relationship between the vertices. I.E. The edges

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474136#content

    screenshot 2024 04 07 at 2.52.29e280afpm
    screenshot 2024 04 07 at 2.54.34e280afpm
    enum GraphType {
        case directed
        case undirected
    }
    
    protocol Graph {
        func addEdge(src: Int, dst: Int)
        func getAdjacentVertices(vertext: Int) -> [Int]?
    }
    
    class AdjacencyMatrix: Graph {
        private var matrix: [[Int]]
        private let type: GraphType
        
        init(vertices: [Int], graphType: GraphType) {
            matrix = Array(
                repeating: Array(
                    repeating: 0,
                    count: vertices.count
                ), count: vertices.count
            )
            type = graphType
        }
        
        func addEdge(src: Int, dst: Int) {
            let range = 0..<matrix[0].count
            guard range ~= src, range ~= dst else {
                return
            }
            matrix[src][dst] = 1
            if type == .undirected {
                matrix[dst][src] = 1
            }
        }
        
        func getAdjacentVertices(vertext: Int) -> [Int]? {
            let range = 0..<matrix[0].count
            guard range ~= vertext else {
                return nil
            }
            var result = [Int]()
            for i in 0..<matrix[0].count {
                //If 1 is present in the cell it means that the vertext V is directly connected to another vertex
                if matrix[vertext][i] == 1 {
                    result.append(i)
                }
            }
            //Always return the vertices in ascending order - to compare with other values's adjacents
            return result.sorted(by: { $0 < $1 })
        }
    }
    

    Adjacency List

    Each vertex is a node

    Each vertex has a pointer to a linked list

    This linked list contains all the other nodes this vertex connects to directly

    If a vertex V has an edge leading to another vertex U

    The U is present in V’s linked list

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474138#content

    Directed Graph – Adjacency List

    I’ll skip implementation of adjacency list. Use 2D Matrix or Set, It’s the simple and easy to handle logics.

    screenshot 2024 04 07 at 3.14.09e280afpm
    screenshot 2024 04 07 at 3.16.16e280afpm

    Cons

    • Order of the vertices in the adjacency lists matter
    • The same graph can have multiple representations

    Certain operations become tricky. E.G. Deleting a node involves looking through all the adjacency lists to remove the node from all lists

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474138#content

    Adjacency Set

    This is very similar to an adjacency list

    Instead of a linked list to maintain the adjacent vertices It use a Set

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474138#content

    screenshot 2024 04 07 at 3.25.18e280afpm
    enum GraphType {
        case directed
        case undirected
    }
    
    protocol Graph {
        func addEdge(src: Int, dst: Int)
        func getAdjacentVertices(vertext: Int) -> [Int]?
    }
    
    class Vertex {
        let value: Int
        var adjacencySet: Set<Int>
        
        init(value: Int, adjacencySet: Set<Int>) {
            self.value = value
            self.adjacencySet = adjacencySet
        }
        
        func addEdge(vertext: Int) {
            adjacencySet.insert(vertext)
        }
        
        func getAdjacentVertices() -> [Int] {
            let array = Array(adjacencySet)
            return array.sorted(by: { $0 < $1 })
        }
    }
    
    class GraphAdjacencySet: Graph {
        var vertices: [Vertex]
        let graphType: GraphType
        var numberOfVertice = 0
        
        init(numberOfVertices: Int, graphType: GraphType) {
            self.vertices = [Vertex]()
            for i in 0..<numberOfVertices {
                vertices.append(Vertex(value: i, adjacencySet: []))
            }
            self.numberOfVertice = numberOfVertices
            self.graphType = graphType
        }
        
        func addEdge(src: Int, dst: Int) {
            let range = 0..<numberOfVertice
            guard range ~= src, range ~= dst else {
                return
            }
            vertices[src].addEdge(vertext: dst)
            if graphType == .undirected {
                vertices[dst].addEdge(vertext: src)
            }
        }
        
        func getAdjacentVertices(vertext: Int) -> [Int]? {
            let range = 0..<numberOfVertice
            guard range ~= vertext else {
                return nil
            }
            return vertices[vertext].getAdjacentVertices()
        }
    }

    Comparison of Graph Representations

    Adjacency Matrix

    • This works well when the graph is well connected I.E. Many nodes are connected with many other nodes
    • The overhead of V^2 space is worth it when the number of connections are large.

    Adjacency List/Set

    • A Sparse graph with few connections between nodes might be more efficiently represented using adjacency list or set

    E = Number of Edges

    V = Number of Vertices

    Space

    • Adjacency Matrix -> O(V^2)
    • Adjacency List -> O(E + V)
    • Adjacency Set -> O(E + V)

    Is Edge Present

    • Adjacency Matrix -> O(1), look up in 2D Array
    • Adjacency List -> Degree of V
    • Adjacency Set -> O(LogDegree of V) <- like a binary search…

    Iterate over edges on a vertex

    • Adjacency Matrix -> O(V)
    • Adjacency List -> Degree of V
    • Adjacency Set -> Degree of V

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474144#content

    Graph Traversal

    This is very similar to tree traversal

    Depth-First

    Breadth-First

    One additional wrinkle

    • In A Tree there is only one path from the root to a specific node
    • In A Graph multiple paths can lead from one node to another
    • A Graph can also have cycles, so the same node can be visited multiple times

    In order to avoid infinite looping in a graph we need to keep track of the nodes previously visited

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474146#content

    screenshot 2024 04 07 at 4.17.05e280afpm

    Depth First Traversal

    screenshot 2024 04 07 at 4.31.20e280afpm
    //Vertex is 5
    var visited = Array(repeating: 0, count: 5)
    
    func dfsGraph(graph: Graph, visited: inout [Int], currentVertex: Int) {
        if visited[currentVertex] == 1 {
            return
        }
        visited[currentVertex] = 1
        
        let list = graph.getAdjacentVertices(vertext: currentVertex) ?? []
        for v in list {
            dfsGraph(graph: graph, visited: &visited, currentVertex: v)
        }
        
        //This for loop ensures that all nodes are covered even for an unconnected graph
        for i in 0..<visited.count {
            dfsGraph(graph: graph, visited: &visited, currentVertex: i)
        }
        //Post order Traversal
        print("\(currentVertex)", terminator: "->")
    }
    
    let graph = AdjacencyMatrix(vertices: [0, 1, 2, 3, 4], graphType: .undirected)
    graph.addEdge(src: 0, dst: 1)
    graph.addEdge(src: 0, dst: 2)
    
    graph.addEdge(src: 1, dst: 4)
    graph.addEdge(src: 1, dst: 3)
    graph.addEdge(src: 1, dst: 0)
    
    graph.addEdge(src: 2, dst: 0)
    graph.addEdge(src: 2, dst: 3)
    
    graph.addEdge(src: 3, dst: 2)
    graph.addEdge(src: 3, dst: 1)
    graph.addEdge(src: 3, dst: 4)
    
    graph.addEdge(src: 4, dst: 1)
    graph.addEdge(src: 4, dst: 3)
    
    dfsGraph(graph: graph, visited: &visited, currentVertex: 0)
    
    screenshot 2024 04 07 at 4.30.39e280afpm

    Breadth First Traversal

    Go level wise from the first node

    Add non-visited child nodes to a queue

    Visited = true means the node has been seen before

    Use a visited list to keep track of nodes already visited

    Not using recursive call

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474146#content

    screenshot 2024 04 07 at 4.34.04e280afpm
    func bfsGraph(graph: Graph, visited: inout [Int], currentVertext: Int) {
        var queue = [Int]()
        queue.append(currentVertext)
        
        while !queue.isEmpty {
            let vertex = queue.removeFirst()
            if visited[vertex] == 1 {
                continue
            }
            print("\(vertex)", terminator: "->")
            //MARK Visited
            visited[vertex] = 1
            if let adjacentVertices = graph.getAdjacentVertices(vertext: vertex) {
                for v in adjacentVertices {
                    if visited[v] != 1 {
                        queue.append(v)
                    }
                }
            }
        }
    }
    
    let graph = AdjacencyMatrix(vertices: [0, 1, 2, 3, 4], graphType: .undirected)
    graph.addEdge(src: 0, dst: 1)
    graph.addEdge(src: 0, dst: 2)
    
    graph.addEdge(src: 1, dst: 4)
    graph.addEdge(src: 1, dst: 3)
    graph.addEdge(src: 1, dst: 0)
    
    graph.addEdge(src: 2, dst: 0)
    graph.addEdge(src: 2, dst: 3)
    
    graph.addEdge(src: 3, dst: 2)
    graph.addEdge(src: 3, dst: 1)
    graph.addEdge(src: 3, dst: 4)
    
    graph.addEdge(src: 4, dst: 1)
    graph.addEdge(src: 4, dst: 3)
    
    bfsGraph(graph: graph, visited: &visited, currentVertext: 0)
    screenshot 2024 04 07 at 4.42.59e280afpm

    Check Unconnected Graph – BFS

    🔥 BFS is not using a recursive call. Don’t put below logic inside bfsGraph function!

        //This for loop ensures that all nodes are covered even for an unconnected graph
        for i in 0..<visited.count {
            bfsGraph(graph: graph, visited: &visited, currentVertext: i)
        }

    Exercise 1. Topological Sort

    It is an ordering of vertices in a directed acyclic graph in which each node comes before all the nodes to which

    It has outgoing edges

    A -> B

    • A should come before B

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474156#content

    screenshot 2024 04 07 at 5.10.21e280afpm
    screenshot 2024 04 07 at 5.10.36e280afpm

    Find first vertex

    If there are vertices which has no incoming edge, you can start any of vertex among them.

    screenshot 2024 04 07 at 5.12.56e280afpm

    Indegree

    • Number of inward directed graph edges for a given graph vertex
    • Indegree of vertex 0 is 0

    If there were no vertices with 0 indegree, then there would have been no topological sort -> The graph has a cycle.

    screenshot 2024 04 07 at 5.18.35e280afpm
    screenshot 2024 04 07 at 5.25.23e280afpm
    screenshot 2024 04 07 at 5.25.38e280afpm
    screenshot 2024 04 07 at 5.27.07e280afpm

    It is an ordering of vertices in a Directed Acyclic Graph in which each node comes before all the nodes to which. It has outgoing edges

    Final code for topological sort. (I added 2 functions)

    enum GraphType {
        case directed
        case undirected
    }
    
    protocol Graph {
        func addEdge(src: Int, dst: Int)
        func getAdjacentVertices(vertext: Int) -> [Int]?
    //New functions
        func getIndegree(vertext: Int) -> Int
        var numberOfVertices: Int { get set }
    }
    
    class Vertex {
        let value: Int
        var adjacencySet: Set<Int>
        
        init(value: Int, adjacencySet: Set<Int>) {
            self.value = value
            self.adjacencySet = adjacencySet
        }
        
        func addEdge(vertext: Int) {
            adjacencySet.insert(vertext)
        }
        
        func getAdjacentVertices() -> [Int] {
            let array = Array(adjacencySet)
            return array.sorted(by: { $0 < $1 })
        }
    }
    

    Using Matrix

    class AdjacencyMatrix: Graph {
        var numberOfVertices: Int
        private var matrix: [[Int]]
        private let type: GraphType
        
        init(vertices: [Int], graphType: GraphType) {
            matrix = Array(
                repeating: Array(
                    repeating: 0,
                    count: vertices.count
                ), count: vertices.count
            )
            numberOfVertices = vertices.count
            type = graphType
        }
        
        func addEdge(src: Int, dst: Int) {
            let range = 0..<numberOfVertices
            guard range ~= src, range ~= dst else {
                return
            }
            matrix[src][dst] = 1
            if type == .undirected {
                matrix[dst][src] = 1
            }
        }
        
        func getAdjacentVertices(vertext: Int) -> [Int]? {
            let range = 0..<numberOfVertices
            guard range ~= vertext else {
                return nil
            }
            var result = [Int]()
            for i in 0..<numberOfVertices {
                //If 1 is present in the cell it means that the vertext V is directly connected to another vertex
                if matrix[vertext][i] == 1 {
                    result.append(i)
                }
            }
            //Always return the vertices in ascending order - to compare with other values's adjacents
            return result.sorted(by: { $0 < $1 })
        }
        
        //New function, Get Indegree of vertex
        func getIndegree(vertext: Int) -> Int {
            let range = 0..<numberOfVertices
            guard range ~= vertext else {
                return 0
            }
            var indegree = 0
            //Loop from 0 to all vertex
            for i in 0..<numberOfVertices {
                //src = i, dst = vertex
                if matrix[i][vertext] != 0 {
                    indegree += 1
                }
            }
            return indegree
        }
    }

    Topological Sort

    func topologicalSort(graph: Graph) -> [Int] {
        var queue = [Int]()
        var indgreeMap = [Int: Int]()
        
        //Step 1. Add first node of sort
        for i in 0..<graph.numberOfVertices {
            let indegree = graph.getIndegree(vertext: i)
            //Add indegree 0's vertex to queue
            indgreeMap[i] = indegree
            //Add all vertices with indegree = 0 to the queue of vertices to explore
            if indegree == 0 {
                queue.append(i)
            }
        }
        
        print(indgreeMap)
        var result = [Int]()
        //BFS
        while !queue.isEmpty {
            let vertex = queue.removeLast()
            result.append(vertex)
            var adjacentVertices = graph.getAdjacentVertices(vertext: vertex) ?? []
            print("v: \(vertex) -> neighbors: \(adjacentVertices)")
            for v in adjacentVertices {
                //Important! get indegree from dictionary!
                //Get the adjacent vertices of the current one and decrement their indegrees by 1
                let updatedIndegree = indgreeMap[v]! - 1
                indgreeMap[v] = updatedIndegree
                //For every vertex which now has indegree = 0, It's a potential next node for the topological sort
                if updatedIndegree == 0 {
                    queue.append(v)
                }
            }
        }
        
        print(result)
        //If the final sorted list is not same to the number of vertices in the graph there is a cycles
        if result.count != graph.numberOfVertices {
            print("There is cycle")
            return []
        }
        
        return result
    }

    Topological sort using Matrix

    screenshot 2024 04 07 at 7.38.29e280afpm
    let graph = AdjacencyMatrix(vertices: [0, 1, 2, 3, 4], graphType: .directed)
    graph.addEdge(src: 0, dst: 1)
    graph.addEdge(src: 0, dst: 2)
    
    graph.addEdge(src: 1, dst: 4)
    
    graph.addEdge(src: 2, dst: 3)
    
    graph.addEdge(src: 3, dst: 1)
    graph.addEdge(src: 3, dst: 4)
    
    topologicalSort(graph: graph)
    
    screenshot 2024 04 07 at 7.38.53e280afpm

    Using Set

    class GraphAdjacencySet: Graph {
        var vertices: [Vertex]
        let graphType: GraphType
        var numberOfVertices: Int
        init(numberOfVertices: Int, graphType: GraphType) {
            self.vertices = [Vertex]()
            for i in 0..<numberOfVertices {
                vertices.append(Vertex(value: i, adjacencySet: []))
            }
            self.numberOfVertices = numberOfVertices
            self.graphType = graphType
        }
        
        func addEdge(src: Int, dst: Int) {
            let range = 0..<numberOfVertices
            guard range ~= src, range ~= dst else {
                return
            }
            vertices[src].addEdge(vertext: dst)
            if graphType == .undirected {
                vertices[dst].addEdge(vertext: src)
            }
        }
        
        func getAdjacentVertices(vertext: Int) -> [Int]? {
            let range = 0..<numberOfVertices
            guard range ~= vertext else {
                return nil
            }
            return vertices[vertext].getAdjacentVertices()
        }
        
        //New function, Get Indegree of vertex
        func getIndegree(vertext: Int) -> Int {
            let range = 0..<numberOfVertices
            guard range ~= vertext else {
                return 0
            }
            var indegree = 0
            //Loop from 0 to all vertex
            for i in 0..<numberOfVertices {
                if getAdjacentVertices(vertext: i)?.contains(vertext) == true {
                    indegree += 1
                }
            }
            return indegree
        }
    }
    
    let graph = GraphAdjacencySet(numberOfVertices: 5, graphType: .directed)
    
    graph.addEdge(src: 0, dst: 1)
    graph.addEdge(src: 0, dst: 2)
    
    graph.addEdge(src: 1, dst: 4)
    
    graph.addEdge(src: 2, dst: 3)
    
    graph.addEdge(src: 3, dst: 1)
    graph.addEdge(src: 3, dst: 4)
    
    topologicalSort(graph: graph)
    screenshot 2024 04 07 at 7.43.34e280afpm

    As you can see the results are the same. You don’t need to change topological sort logic, Because both Set and Matrix implements Graph interface.

    Exercise 2. Design A Course Schedule Considering Pre-reqs for Courses

    Design a schedule for a student to complete her degree given the list of courses along with the prerequisites for each course

    We have 2 lists

    • List of courses
    • List of pre-reqs for each course

    This contains pairs (course A, course B) where course A, course B belong to courses list and course A should be taken before course B

    We want to know a valid order in which the student can take her courses!

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474160#content

    Each course can be a vertex

    screenshot 2024 04 07 at 7.59.01e280afpm
    screenshot 2024 04 07 at 8.02.59e280afpm

    Any course that has pre-reqs should not come before its pre-reqs in the schedule!

    It is an ordering of vertices in a directed acyclic graph in which each node comes before all the nodes to which it has outgoing edges

    It’s a Topological Sort!

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474160#content

    screenshot 2024 04 07 at 8.43.14e280afpm

    All nodes with indegree 0 are potential courses the student could start with

    There are many schedules possible!

    func courseOrder(courseList: [Int], prereqs: [Int: [Int]]) {
        let graph = AdjacencyMatrix(vertices: courseList, graphType: .directed)
        //You need this because Matrix used row and col start from 0 to number of vertices.
        var courseId = [Int: Int]()
        var matrixIndexToCourseId = [Int: Int]()
        
        for (index, course) in courseList.enumerated() {
            courseId[course] = index
            matrixIndexToCourseId[index] = course
        }
        
        //Link Edges
        for (preCourse, nextCourse) in prereqs {
            for next in nextCourse {
                print("add edge: \(preCourse) -> \(next)")
                let preCourseId = courseId[preCourse]
                let nextId = courseId[next]
                graph.addEdge(src: preCourseId!, dst: nextId!)
            }
        }
        var indegreeMap = [Int: Int]()
        //Int is courseId not a Index
        var queue = [Int]()
        var sorted = [Int]()
        
        for v in courseList {
            //VertextId will be 0..<numberOfVertices
            let vertexId = courseId[v]!
            let indegree = graph.getIndegree(vertext: vertexId)
            indegreeMap[v] = indegree
            //Can take this course
            if indegree == 0 {
                queue.append(v)
            }
        }
        print(indegreeMap)
        while !queue.isEmpty {
            let insertingCourse = queue.removeFirst()
            sorted.append(insertingCourse)
            let vertexId = courseId[insertingCourse]!
            let nextCourses = graph.getAdjacentVertices(vertext: vertexId) ?? []
            for nextCourse in nextCourses {
                let courseId = matrixIndexToCourseId[nextCourse]!
                let updatedIndegree = indegreeMap[courseId]! - 1
                indegreeMap[courseId] = updatedIndegree
                print("Next course: \(courseId) - Indegree: \(updatedIndegree)")
                if updatedIndegree == 0 {
                    queue.append(courseId)
                }
            }
        }
        print(sorted)
    }
    
    courseOrder(
        courseList: [101, 102, 103, 104, 105, 106, 107],
        prereqs: [101: [102, 103, 105], 104: [105], 105: [107]]
    )
    
    screenshot 2024 04 07 at 8.59.44e280afpm

  • iOS, Heap Data Structure

    iOS, Heap Data Structure

    ✍️ Note

    Some codes and contents are sourced from Udemy. This post is for personal notes where I summarize the original contents to grasp the key concepts (🎨 some images I draw it)

    Priority Queue

    When a certain element in a collection has the highest weightage or priority – A common use case is to process that First

    The data structure you use to store elements where the highest priority has to be processed first can be called a priority queue

    At every step we access the element with the highest priority

    The data structure needs to understand the priorities of the elements it holds

    Common Operations

    • Insert elements (along with priority information)
    • Access the highest priority element
    • Remove the highest priority element

    Use cases

    • Event simulation, Thread scheduling

    Implementation

    • Case 1. Use an Array or List
      • Unordered
        • Insertion
          • Can be anywhere in the list or array – O(1)
        • Access
          • Accessing the highest priority element requires going through all elements in the List – O(N)
        • Remove
          • Removing the highest priority element requires going through all elements in the list – O(N)
      • Ordered
        • Insertion
          • Requires finding the right position for the element based on priority – O(N)
        • Access
          • Accessing the highest priority element is then easy – O(1)
        • Remove
          • Removing the highest priority element is straightforward – O(1)
    • Case 2. Balanced Binary Search Tree – All operations are O(LogN)
      • Insertion
        • Worst Case – O(LogN)
      • Access
        • Accessing the highest priority element is again O(LogN)
      • Remove
        • O(LogN)
    • Case 3. Binary Heap is the best for implementing Priority Queue
      • Insertion – O(LogN)
      • Access – O(1)
      • Remove – O(LogN)

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8473688#overview

    Binary Heap

    A heap is just a tree with a special properties or constraints on the values of its nodes

    2 types

    • Minimum Heap
      • Root is the smallest value
      • Every node value should be <= value of it’s child
    • Maximum Heap
      • Root is the maximum value
      • Every node value should be >= value of it’s child

    Constraints

    • Shape Property: If H is the Height of the Tree
      • THe leaf nodes should be only be at level H or H-1
    • Heap should from a complete binary Tree
      • All levels except the last should be filled

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8473690#content

    screenshot 2024 04 03 at 9.40.56e280afpm

    Shape Property, Height

    screenshot 2024 04 03 at 9.53.18e280afpm

    All nodes at Level H-1 nodes (9, 12, 11, 7) have to be filled before moving on to level H

    Implementation

    Logical structure of a binary heap is a Tree

    Each node would have a pointer to the left and right child

    Traverse

    • Traverse downwards from the root towards the leaf nodes
    • Traverse upward from the leaf nodes towards the root

    Operations

    • Get left child
    • Get right child
    • Get parent

    Heaps can be represented much more efficiently by using an array and having an implicit relationship to determine the parent, left and right child of a node

    Every level of the binary tree in a heap is filled except perhaps the last

    This means contiguous slots in an array can be used to represent binary tree levels

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8473692#content

    screenshot 2024 04 03 at 10.16.45e280afpm

    Heapify

    While inserting or removing an element into the heap How do we know which is the right position for the element to occupy?

    HEAPIFY

    • We place a single element in the wrong position
    • Then we try and find the right position for the element

    Operations

    • Sift Down
      • An element is in the wrong position with respect to other elements below it in the heap
      • It has to be moved downwards in the heap towards the leaf nodes to find it’s right position
    • Sift Up
      • An element is in the wrong position with respect to other elements above it in the heap
      • It has to be moved upwards in the heap towards the root node to find it’s right position

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8473704#content

    HEAPIFY – Sift Down

    screenshot 2024 04 03 at 10.48.21e280afpm
    protocol Heap {
        associatedtype Item: Comparable
        var array: [Item?] { get set }
        var count: Int { get set }
        
        init(_ input: [Item])
        
        func leftChildIndex(index: Int) -> Int?
        func rightChildIndex(index: Int) -> Int?
        func parentIndex(index: Int) -> Int?
        func siftDown(index: Int)
    }
    
    extension Heap {
        func leftChildIndex(index: Int) -> Int? {
            let leftIndex = 2 * index + 1
            if leftIndex >= count {
                return nil
            }
            return leftIndex
        }
        
        func rightChildIndex(index: Int) -> Int? {
            let rightIndex = 2 * index + 2
            if rightIndex >= count {
                return nil
            }
            return rightIndex
        }
        
        func parentIndex(index: Int) -> Int? {
            if index < 0 || index > count {
                return nil
            }
            return (index - 1) / 2
        }
        
        //MARK: Helper functions
        var isFull: Bool {
            count == array.count
        }
    }
    
    class MinHeap<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        
        required init(_ input: [T]) {
            self.array = input
            self.count = input.count
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let leftChildIndex = leftChildIndex(index: index)
            let rightChildIndex = rightChildIndex(index: index)
            
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                if array[leftChildIndex!]! < array[rightChildIndex!]! {
                    swapCandidateIndex = leftChildIndex
                }
                else {
                    swapCandidateIndex = rightChildIndex
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            if currentIndexValue > swapCandidateValue {
                let value = array[swapCandidateIndex]
                //Sift down large value to the next level
                array[swapCandidateIndex] = currentIndexValue
                array[index] = swapCandidateValue
                
                //Recursive Call
                siftDown(index: swapCandidateIndex)
            }
        }
    }
    
    let minHeap = MinHeap([13, 8, 6, 9, 12, 11, 7, 15, 10])
    
    minHeap.siftDown(index: 0)
    print(minHeap.array)
    
    screenshot 2024 04 03 at 11.11.57e280afpm

    HEAPIFY – Sift Up

    screenshot 2024 04 03 at 11.18.39e280afpm

    Sift Up is much simpler than Sift Down

    func siftUp(index: Int) {
            guard let parentIndex = parentIndex(index: index), let currentValue = array[index] else {
                return
            }
            if array[parentIndex]! > currentValue {
                let parentValue = array[parentIndex]!
                array[parentIndex] = currentValue
                array[index] = parentValue
                siftUp(index: parentIndex)
            }
        }
    screenshot 2024 04 03 at 11.24.08e280afpm

    Insert

    Insert an element as a Leaf Node in the Heap

    In an array implementation that would be at the very end – The newly inserted element would be the last element in the array

    The element might be in the wrong position with respect to all nodes above it

    It has to be moved Upwards in the heap towards the root node to find it’s right position

    SIFT UP

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8473710?start=30#content

    screenshot 2024 04 04 at 2.48.33e280afpm
    func insert(value: Item) {
        array.append(value)
        count = array.count
        let insertedIndex = array.count - 1
        siftUp(index: insertedIndex)
    }
    screenshot 2024 04 04 at 2.51.23e280afpm

    Remove

    Remove the highest priority element in the heap I.E The minimum element

    In an array implementation that would be the element at index 0

    Copy over the last element in the array to index 0

    The element might be in the wrong position with respect to all nodes below it

    It has to be moved downwards in the heap towards the leaf nodes to find it’s right position

    SIFT DOWN

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8473710?start=30#content

    screenshot 2024 04 04 at 3.03.38e280afpm
    screenshot 2024 04 04 at 3.03.52e280afpm
    func removeHighestPriority() -> T? {
        let root = array[0]
        let lastElement = array[array.count - 1]
        array.removeFirst()
        array.insert(lastElement, at: 0)
        count = array.count
        siftDown(index: 0)
        return root
    }
    screenshot 2024 04 04 at 3.07.21e280afpm

    I rewrote removeHighestPriority. Because removeFirst() It took O(N).

    func removeHighestPriority() -> T? {
        //swapAt is O(1), Now root is last item
        array.swapAt(0, array.count - 1)
        //removeLast() is O(1)
        array.removeLast()
        count = array.count
        siftDown(index: 0)
        return root
    }

    Binary Heap Complexity

    Insertion – Inserting a new element O(logN)

    Access – Accessing the Highest priority element O(1)

    Remove – Removing the highest priority element is O(logN)

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8473710?start=30#content

    Heap Sort

    Uses a heap to help sort elements in ascending or descending order

    Step 1. Heapify: First converts the unsorted list or array into a heap – This can be done in place (No needs additional spaces)

    • Take a portion of the array – Make all elements in that portion satisfy the heap property
    • Keep adding additional elements into the heap portion ensuring that these additional elements also satisfy the heap property
    • The heap will grow to encompass all elements in the array

    Step 2. Sort: Use the heap to access the maximum element and put it in the right position in the array (Heap to sorted array)

    • A heap offers O(1) access to the largest or the smallest element
    • Remove the largest element from the heap and position it at the end of the sorted array
    • The sorted array will grow to encompass all elements in the array

    Implementation

    • We’ll use a maximum heap so we can always access the largest element in O(1) time
    • A heap can be represented using an array
    • Heapify is the operation to convert the unsorted array to a heap
    • We use the same array with no additional space to do the heapify

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474078#content

    screenshot 2024 04 04 at 4.35.06e280afpm
    screenshot 2024 04 04 at 4.35.23e280afpm
    screenshot 2024 04 04 at 4.35.58e280afpm
    screenshot 2024 04 04 at 4.36.18e280afpm
    screenshot 2024 04 04 at 4.38.42e280afpm
    class MaxHeap<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        
        required init(_ input: [T]) {
            self.array = input
            self.count = input.count
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let leftChildIndex = leftChildIndex(index: index)
            let rightChildIndex = rightChildIndex(index: index)
            
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                if array[leftChildIndex!]! > array[rightChildIndex!]! {
                    swapCandidateIndex = leftChildIndex
                }
                else {
                    swapCandidateIndex = rightChildIndex
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            if currentIndexValue > swapCandidateValue {
                let value = array[swapCandidateIndex]
                //Sift down small value to the next level
                array[swapCandidateIndex] = currentIndexValue
                array[index] = swapCandidateValue
                
                //Recursive Call
                siftDown(index: swapCandidateIndex)
            }
        }
        
        func siftUp(index: Int) {
            guard let parentIndex = parentIndex(index: index), let currentValue = array[index] else {
                return
            }
            if array[parentIndex]! < currentValue {
                let parentValue = array[parentIndex]!
                array[parentIndex] = currentValue
                array[index] = parentValue
                siftUp(index: parentIndex)
            }
        }
        
        
        func insert(value: Item) {
            array.append(value)
            count = array.count
            let insertedIndex = array.count - 1
            siftUp(index: insertedIndex)
        }
        
        
        func removeHighestPriority() -> T? {
            let root = array[0]
            let lastElement = array[array.count - 1]
            array.removeFirst()
            array.insert(lastElement, at: 0)
            siftDown(index: 0)
            return root
        }
        
        //Heapify the entire array
        func heapify(index: Int) {
            //Start with the parent index of the last element
            guard var parentIndex = parentIndex(index: index) else {
                return
            }
            while parentIndex >= 0 {
                percolateDown(index: parentIndex)
                parentIndex -= 1
            }
        }
        
        func percolateDown(index: Int) {
            let leftIndex = leftChildIndex(index: index)
            let rightIndex = rightChildIndex(index: index)
            
            //Check If the max heap property is satisfied
            if let leftIndex, array[leftIndex]! > array[index]! {
                let currentValue = array[index]!
                let leftChildValue = array[leftIndex]!
                array[index] = leftChildValue
                array[leftIndex] = currentValue
                percolateDown(index: leftIndex)
            }
            if let rightIndex, array[rightIndex]! > array[index]! {
                let currentValue = array[index]!
                let rightChildValue = array[rightIndex]!
                array[index] = rightChildValue
                array[rightIndex] = currentValue
                percolateDown(index: rightIndex)
            }
        }
    }
    
    let input = [4, 6, 9, 2, 10, 56, 12, 5, 1, 17, 14]
    let maxHeap = MaxHeap(input)
    
    maxHeap.heapify(index: input.count - 1)
    
    print(maxHeap.array)
    
    screenshot 2024 04 04 at 5.46.05e280afpm

    Heap Sort – Heap to Sorted Array

    screenshot 2024 04 04 at 6.05.20e280afpm

    Full source – (This code has updated previous example)

    protocol Heap {
        associatedtype Item: Comparable
        var array: [Item?] { get set }
        var count: Int { get set }
        
        init(_ input: [Item])
        
        func leftChildIndex(index: Int, endIndex: Int) -> Int?
        func rightChildIndex(index: Int, endIndex: Int) -> Int?
        func parentIndex(index: Int, endIndex: Int) -> Int?
        func siftDown(index: Int)
        func siftUp(index: Int)
        func insert(value: Item)
        func removeHighestPriority() -> Item?
    }
    
    extension Heap {
        func leftChildIndex(index: Int, endIndex: Int) -> Int? {
            let leftIndex = 2 * index + 1
            if leftIndex > endIndex {
                return nil
            }
            return leftIndex
        }
        
        func rightChildIndex(index: Int, endIndex: Int) -> Int? {
            let rightIndex = 2 * index + 2
            if rightIndex > endIndex {
                return nil
            }
            return rightIndex
        }
        
        func parentIndex(index: Int, endIndex: Int) -> Int? {
            if index < 0 || index > endIndex {
                return nil
            }
            return (index - 1) / 2
        }
        
        //MARK: Helper functions
        var isFull: Bool {
            count == array.count
        }
    }
    
    class MinHeap<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        
        required init(_ input: [T]) {
            self.array = input
            self.count = input.count
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let leftChildIndex = leftChildIndex(index: index, endIndex: count)
            let rightChildIndex = rightChildIndex(index: index, endIndex: count)
            
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                if array[leftChildIndex!]! < array[rightChildIndex!]! {
                    swapCandidateIndex = leftChildIndex
                }
                else {
                    swapCandidateIndex = rightChildIndex
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            if currentIndexValue > swapCandidateValue {
                let value = array[swapCandidateIndex]
                //Sift down large value to the next level
                array[swapCandidateIndex] = currentIndexValue
                array[index] = swapCandidateValue
                
                //Recursive Call
                siftDown(index: swapCandidateIndex)
            }
        }
        
        func siftUp(index: Int) {
            guard let parentIndex = parentIndex(index: index, endIndex: count), let currentValue = array[index] else {
                return
            }
            if array[parentIndex]! > currentValue {
                let parentValue = array[parentIndex]!
                array[parentIndex] = currentValue
                array[index] = parentValue
                siftUp(index: parentIndex)
            }
        }
        
        
        func insert(value: Item) {
            array.append(value)
            count = array.count
            let insertedIndex = array.count - 1
            siftUp(index: insertedIndex)
        }
        
        
        func removeHighestPriority() -> T? {
            let root = array[0]
            let lastElement = array[array.count - 1]
            array.removeFirst()
            array.insert(lastElement, at: 0)
            siftDown(index: 0)
            return root
        }
    }
    
    class MaxHeap<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        
        required init(_ input: [T]) {
            self.array = input
            self.count = input.count
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let leftChildIndex = leftChildIndex(index: index, endIndex: count)
            let rightChildIndex = rightChildIndex(index: index, endIndex: count)
            
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                if array[leftChildIndex!]! > array[rightChildIndex!]! {
                    swapCandidateIndex = leftChildIndex
                }
                else {
                    swapCandidateIndex = rightChildIndex
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            if currentIndexValue > swapCandidateValue {
                let value = array[swapCandidateIndex]
                //Sift down small value to the next level
                array[swapCandidateIndex] = currentIndexValue
                array[index] = swapCandidateValue
                
                //Recursive Call
                siftDown(index: swapCandidateIndex)
            }
        }
        
        func siftUp(index: Int) {
            guard let parentIndex = parentIndex(index: index, endIndex: count), let currentValue = array[index] else {
                return
            }
            if array[parentIndex]! < currentValue {
                let parentValue = array[parentIndex]!
                array[parentIndex] = currentValue
                array[index] = parentValue
                siftUp(index: parentIndex)
            }
        }
        
        
        func insert(value: Item) {
            array.append(value)
            count = array.count
            let insertedIndex = array.count - 1
            siftUp(index: insertedIndex)
        }
        
        
        func removeHighestPriority() -> T? {
            let root = array[0]
            let lastElement = array[array.count - 1]
            array.removeFirst()
            array.insert(lastElement, at: 0)
            siftDown(index: 0)
            return root
        }
        
        //Heapify the entire array
        func heapify(index: Int) {
            //Start with the parent index of the last element
            guard var parentIndex = parentIndex(index: index, endIndex: index) else {
                return
            }
            while parentIndex >= 0 {
                percolateDown(index: parentIndex, endIndex: index)
                parentIndex -= 1
            }
        }
        
        func percolateDown(index: Int, endIndex: Int) {
            let leftIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightIndex = rightChildIndex(index: index, endIndex: endIndex)
            
            //Check If the max heap property is satisfied
            if let leftIndex, array[leftIndex]! > array[index]! {
                let currentValue = array[index]!
                let leftChildValue = array[leftIndex]!
                array[index] = leftChildValue
                array[leftIndex] = currentValue
                percolateDown(index: leftIndex, endIndex: endIndex)
            }
            if let rightIndex, array[rightIndex]! > array[index]! {
                let currentValue = array[index]!
                let rightChildValue = array[rightIndex]!
                array[index] = rightChildValue
                array[rightIndex] = currentValue
                percolateDown(index: rightIndex, endIndex: endIndex)
            }
        }
        
        func heapSort() {
            //Heapify! The entire unsorted array so It's now a Heap
            heapify(index: array.count - 1)
            var lastIndex = array.count - 1
            while lastIndex > 0 {
                let maxValue = array[0]!
                let currentValue = array[lastIndex]!
                //Swap - Start with the very last index, and place the largest element in the last position
                array[0] = currentValue
                array[lastIndex] = maxValue
                
                //Count down because last element already sorted
                lastIndex -= 1
                percolateDown(index: 0, endIndex: lastIndex)
            }
        }
    }
    
    let input = [4, 6, 9, 2, 10, 56, 12, 5, 1, 17, 14]
    let maxHeap = MaxHeap(input)
    
    print("Heapify - Make unordered input to maxHeap: ")
    maxHeap.heapify(index: input.count - 1)
    
    print(maxHeap.array)
    maxHeap.heapSort()
    print("\nHeap Sort!: ")
    print(maxHeap.array)
    screenshot 2024 04 04 at 10.11.13e280afpm

    Heap Sort

    Heap sort uses operations on a heap to get a sorted list

    Insertion into a heap is done N times to get all the elements in heap form

    Removal of the maximum element is done N times, followed by Heapify

    Insertion and removal have log N time complexity so doing it for N elements means

    • The average case complexity of heap sort is O(nLogN)

    Heap sort is not adaptive – If works on almost sorted array it’s not a fast because there is no way to break up early.

    It’s not a stable sort – If there is 2 same value.. that order is not maintain in heap sort. It means 2 same value can’t guarantee orders. (But heap sort can handle if there are same values) see stackoverflow

    It does not need additional space – Space complexity is O(1)

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474084#content

    My example codes for Heap Structure is not the same. Some exercise I used the fixed array using maxSize. The others are not. Please check full source code at each exercise.

    Exercise 1. Find the maximum element in a minimum heap

    screenshot 2024 04 05 at 1.49.58e280afpm
    class MinHeap<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        
        required init(_ input: [T]) {
            self.array = input
            self.count = input.count
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let leftChildIndex = leftChildIndex(index: index, endIndex: count)
            let rightChildIndex = rightChildIndex(index: index, endIndex: count)
            
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                if array[leftChildIndex!]! < array[rightChildIndex!]! {
                    swapCandidateIndex = leftChildIndex
                }
                else {
                    swapCandidateIndex = rightChildIndex
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            if currentIndexValue > swapCandidateValue {
                let value = array[swapCandidateIndex]
                //Sift down large value to the next level
                array[swapCandidateIndex] = currentIndexValue
                array[index] = swapCandidateValue
                
                //Recursive Call
                siftDown(index: swapCandidateIndex)
            }
        }
        
        func siftUp(index: Int) {
            guard let parentIndex = parentIndex(index: index, endIndex: count), let currentValue = array[index] else {
                return
            }
            if array[parentIndex]! > currentValue {
                let parentValue = array[parentIndex]!
                array[parentIndex] = currentValue
                array[index] = parentValue
                siftUp(index: parentIndex)
            }
        }
        
        
        func insert(value: Item) {
            array.append(value)
            count = array.count
            let insertedIndex = array.count - 1
            siftUp(index: insertedIndex)
        }
        
        
        func removeHighestPriority() -> T? {
            let root = array[0]
            let lastElement = array[array.count - 1]
            array.removeFirst()
            array.insert(lastElement, at: 0)
            siftDown(index: 0)
            return root
        }
        
        //Heapify the entire array
        func heapify(index: Int) {
            //Start with the parent index of the last element
            guard var parentIndex = parentIndex(index: index, endIndex: index) else {
                return
            }
            while parentIndex >= 0 {
                percolateDown(index: parentIndex, endIndex: index)
                parentIndex -= 1
            }
        }
        
        func percolateDown(index: Int, endIndex: Int) {
            let leftIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightIndex = rightChildIndex(index: index, endIndex: endIndex)
            
            //Check If the max heap property is satisfied
            if let leftIndex, array[leftIndex]! < array[index]! {
                let currentValue = array[index]!
                let leftChildValue = array[leftIndex]!
                array[index] = leftChildValue
                array[leftIndex] = currentValue
                percolateDown(index: leftIndex, endIndex: endIndex)
            }
            if let rightIndex, array[rightIndex]! < array[index]! {
                let currentValue = array[index]!
                let rightChildValue = array[rightIndex]!
                array[index] = rightChildValue
                array[rightIndex] = currentValue
                percolateDown(index: rightIndex, endIndex: endIndex)
            }
        }
        
        func maxValue() -> T? {
            let lastIndex = array.count - 1
            guard let parentIndex = parentIndex(index: lastIndex, endIndex: array.count - 1), var maxValue = array[parentIndex + 1] else {
                return nil
            }
            //maxValue is first leaf node
            let firstLeafIndex = parentIndex + 1
            for i in firstLeafIndex..<array.count {
                maxValue = max(array[i]!, maxValue)
            }
            return maxValue
        }
    }
    
    let input = [4, 6, 9, 2, 10, 56, 12, 5, 1, 17, 14]
    let minHeap = MinHeap(input)
    
    print("Heapify - Make unordered input to maxHeap: ")
    minHeap.heapify(index: input.count - 1)
    print(minHeap.array)
    
    print("🟢 Max Value in MinHeap: \(minHeap.maxValue())")
    screenshot 2024 04 05 at 1.57.38e280afpm

    Exercise 2. Find K Largest Elements in a Stream

    Stream can be events from external sources. It is not a sorted

    Use a minimum heap with size K to store elements as they come in

    The top of the heap will have the smallest of the K elements stored in the heap

    If the new elements in the stream is larger than the minimum – add it to the heap

    The heap will always have the largest K elements from the stream

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474098?start=150#content

    screenshot 2024 04 05 at 2.19.47e280afpm
    screenshot 2024 04 05 at 2.20.25e280afpm
    screenshot 2024 04 05 at 2.20.40e280afpm
    protocol Heap {
        associatedtype Item: Comparable
        var array: [Item?] { get set }
        var count: Int { get set }
        var maxSize: Int { get set }
        
        init(maxSize: Int)
        
        func leftChildIndex(index: Int, endIndex: Int) -> Int?
        func rightChildIndex(index: Int, endIndex: Int) -> Int?
        func parentIndex(index: Int, endIndex: Int) -> Int?
        func siftDown(index: Int)
        func siftUp(index: Int)
        func insert(value: Item)
        func removeHighestPriority() -> Item?
        func highestPriority() -> Item?
    }
    
    extension Heap {
        func leftChildIndex(index: Int, endIndex: Int) -> Int? {
            let leftIndex = 2 * index + 1
            if leftIndex > endIndex {
                return nil
            }
            return leftIndex
        }
        
        func rightChildIndex(index: Int, endIndex: Int) -> Int? {
            let rightIndex = 2 * index + 2
            if rightIndex > endIndex {
                return nil
            }
            return rightIndex
        }
        
        func parentIndex(index: Int, endIndex: Int) -> Int? {
            if index < 0 || index > endIndex {
                return nil
            }
            return (index - 1) / 2
        }
        
        //MARK: Helper functions
        var isFull: Bool {
            count == array.count
        }
        
        func highestPriority() -> Item? {
            guard count > 0, let firstItem = array.first else {
                return nil
            }
            return firstItem
        }
    }
    
    class MinHeap<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        var maxSize: Int
        
        required init(maxSize: Int) {
            self.array = Array(repeating: nil, count: maxSize)
            self.count = 0
            self.maxSize = maxSize
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let endIndex = min(count, maxSize)
            let leftChildIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightChildIndex = rightChildIndex(index: index, endIndex: endIndex)
            
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                if array[leftChildIndex!]! < array[rightChildIndex!]! {
                    swapCandidateIndex = leftChildIndex
                }
                else {
                    swapCandidateIndex = rightChildIndex
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            if currentIndexValue > swapCandidateValue {
                let value = array[swapCandidateIndex]
                //Sift down large value to the next level
                array[swapCandidateIndex] = currentIndexValue
                array[index] = swapCandidateValue
                
                //Recursive Call
                siftDown(index: swapCandidateIndex)
            }
        }
        
        func siftUp(index: Int) {
            let endIndex = min(count, maxSize)
            guard let parentIndex = parentIndex(index: index, endIndex: endIndex), let currentValue = array[index] else {
                return
            }
            if array[parentIndex]! > currentValue {
                let parentValue = array[parentIndex]!
                array[parentIndex] = currentValue
                array[index] = parentValue
                siftUp(index: parentIndex)
            }
        }
        
        
        func insert(value: Item) {
            //MARK: Check isHeap Full or not
            if count >= array.count {
                return
            }
            array[count] = value
            let insertedIndex = count
            siftUp(index: insertedIndex)
            count += 1
        }
        
        
        func removeHighestPriority() -> T? {
            let root = array[0]
            let lastElement = array[count - 1]
            array.removeFirst()
            array.insert(lastElement, at: 0)
            count -= 1
            siftDown(index: 0)
            return root
        }
        
        //Heapify the entire array
        func heapify(index: Int) {
            //Start with the parent index of the last element
            guard var parentIndex = parentIndex(index: index, endIndex: index) else {
                return
            }
            while parentIndex >= 0 {
                percolateDown(index: parentIndex, endIndex: index)
                parentIndex -= 1
            }
        }
        
        func percolateDown(index: Int, endIndex: Int) {
            let leftIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightIndex = rightChildIndex(index: index, endIndex: endIndex)
            
            //Check If the max heap property is satisfied
            if let leftIndex, array[leftIndex]! < array[index]! {
                let currentValue = array[index]!
                let leftChildValue = array[leftIndex]!
                array[index] = leftChildValue
                array[leftIndex] = currentValue
                percolateDown(index: leftIndex, endIndex: endIndex)
            }
            if let rightIndex, array[rightIndex]! < array[index]! {
                let currentValue = array[index]!
                let rightChildValue = array[rightIndex]!
                array[index] = rightChildValue
                array[rightIndex] = currentValue
                percolateDown(index: rightIndex, endIndex: endIndex)
            }
        }
        
        func maxValue() -> T? {
            let lastIndex = array.count - 1
            guard let parentIndex = parentIndex(index: lastIndex, endIndex: array.count - 1), var maxValue = array[parentIndex + 1] else {
                return nil
            }
            //maxValue is first leaf node
            let firstLeafIndex = parentIndex + 1
            for i in firstLeafIndex..<array.count {
                maxValue = max(array[i]!, maxValue)
            }
            return maxValue
        }
    }
    
    func findKLargestElement() {
        let input = [4, 6, 9, 2, 10, 56, 12, 5, 1, 17, 14]
        let minHeap = MinHeap<Int>(maxSize: 5)
        for data in input {
            if !minHeap.isFull {
                minHeap.insert(value: data)
            }
            else {
                //heap is full
                if let minValue = minHeap.highestPriority(),
                    minValue < data {
                    minHeap.removeHighestPriority()
                    minHeap.insert(value: data)
                }
            }
        }
        
        print(minHeap.array)
        print("🟢 Max Value in MinHeap: \(minHeap.maxValue())")
    }
    
    findKLargestElement()
    

    screenshot 2024 04 05 at 5.15.16e280afpm

    Exercise 3. Merge K sorted arrays using heaps

    Say the lists to be merged are sorted in ascending order

    Remove the smallest element from each list and add it to the minimum heap – There will be K elements in the Heap

    Get the minimum element from the heap – It will be the smallest of all first elements in the K lists

    Keep track of which list this smallest element originally came from

    Get the next smallest element from the same list

    Continue this till the sorted list is completely filled

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474102#content

    screenshot 2024 04 05 at 6.06.29e280afpm
    screenshot 2024 04 05 at 6.06.58e280afpm
    screenshot 2024 04 05 at 6.07.35e280afpm

    As you can see when removed minValue then keep insert newValue from minValue’s original array.

    protocol Heap {
        associatedtype Item: Comparable
        var array: [Item?] { get set }
        var count: Int { get set }
        var maxSize: Int { get set }
        
        init(maxSize: Int)
        
        func leftChildIndex(index: Int, endIndex: Int) -> Int?
        func rightChildIndex(index: Int, endIndex: Int) -> Int?
        func parentIndex(index: Int, endIndex: Int) -> Int?
        func siftDown(index: Int)
        func siftUp(index: Int)
        func insert(value: Item)
        func removeHighestPriority() -> Item?
        func highestPriority() -> Item?
    }
    
    extension Heap {
        func leftChildIndex(index: Int, endIndex: Int) -> Int? {
            let leftIndex = 2 * index + 1
            if leftIndex > endIndex {
                return nil
            }
            return leftIndex
        }
        
        func rightChildIndex(index: Int, endIndex: Int) -> Int? {
            let rightIndex = 2 * index + 2
            if rightIndex > endIndex {
                return nil
            }
            return rightIndex
        }
        
        func parentIndex(index: Int, endIndex: Int) -> Int? {
            if index < 0 || index > endIndex {
                return nil
            }
            return (index - 1) / 2
        }
        
        //MARK: Helper functions
        var isFull: Bool {
            count == array.count
        }
        
        func highestPriority() -> Item? {
            guard count > 0, let firstItem = array.first else {
                return nil
            }
            return firstItem
        }
    }
    
    class MinHeap<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        var maxSize: Int
        
        required init(maxSize: Int) {
            self.array = Array(repeating: nil, count: maxSize)
            self.count = 0
            self.maxSize = maxSize
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let endIndex = min(count, maxSize)
            let leftChildIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightChildIndex = rightChildIndex(index: index, endIndex: endIndex)
            
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                if array[leftChildIndex!]! < array[rightChildIndex!]! {
                    swapCandidateIndex = leftChildIndex
                }
                else {
                    swapCandidateIndex = rightChildIndex
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            if currentIndexValue > swapCandidateValue {
                let value = array[swapCandidateIndex]
                //Sift down large value to the next level
                array[swapCandidateIndex] = currentIndexValue
                array[index] = swapCandidateValue
                
                //Recursive Call
                siftDown(index: swapCandidateIndex)
            }
        }
        
        func siftUp(index: Int) {
            let endIndex = min(count, maxSize)
            guard let parentIndex = parentIndex(index: index, endIndex: endIndex), let currentValue = array[index] else {
                return
            }
            if array[parentIndex]! > currentValue {
                let parentValue = array[parentIndex]!
                array[parentIndex] = currentValue
                array[index] = parentValue
                siftUp(index: parentIndex)
            }
        }
        
        
        func insert(value: Item) {
            //MARK: Check isHeap Full or not
            if count >= array.count {
                return
            }
            array[count] = value
            let insertedIndex = count
            siftUp(index: insertedIndex)
            count += 1
        }
        
        
        func removeHighestPriority() -> T? {
            let root = array[0]
            let lastElement = array[count - 1]
            array.removeFirst()
            array.insert(lastElement, at: 0)
            count -= 1
            siftDown(index: 0)
            return root
        }
        
        //Heapify the entire array
        func heapify(index: Int) {
            //Start with the parent index of the last element
            guard var parentIndex = parentIndex(index: index, endIndex: index) else {
                return
            }
            while parentIndex >= 0 {
                percolateDown(index: parentIndex, endIndex: index)
                parentIndex -= 1
            }
        }
        
        func percolateDown(index: Int, endIndex: Int) {
            let leftIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightIndex = rightChildIndex(index: index, endIndex: endIndex)
            
            //Check If the max heap property is satisfied
            if let leftIndex, array[leftIndex]! < array[index]! {
                let currentValue = array[index]!
                let leftChildValue = array[leftIndex]!
                array[index] = leftChildValue
                array[leftIndex] = currentValue
                percolateDown(index: leftIndex, endIndex: endIndex)
            }
            if let rightIndex, array[rightIndex]! < array[index]! {
                let currentValue = array[index]!
                let rightChildValue = array[rightIndex]!
                array[index] = rightChildValue
                array[rightIndex] = currentValue
                percolateDown(index: rightIndex, endIndex: endIndex)
            }
        }
        
        func maxValue() -> T? {
            let lastIndex = array.count - 1
            guard let parentIndex = parentIndex(index: lastIndex, endIndex: array.count - 1), var maxValue = array[parentIndex + 1] else {
                return nil
            }
            //maxValue is first leaf node
            let firstLeafIndex = parentIndex + 1
            for i in firstLeafIndex..<array.count {
                maxValue = max(array[i]!, maxValue)
            }
            return maxValue
        }
    }
    
    class Element: Comparable {
        let listIndex: Int
        let value: Int
        
        init(listIndex: Int, value: Int) {
            self.listIndex = listIndex
            self.value = value
        }
        
        static func < (lhs: Element, rhs: Element) -> Bool {
            lhs.value < rhs.value
        }
        
        static func == (lhs: Element, rhs: Element) -> Bool {
            lhs.value == rhs.value
        }
    }
    
    //Lists is sorted lists ascending orders
    func mergeKSortedList(total: Int, lists: [[Int]]) {
        let minHeap = MinHeap<Element>(maxSize: lists.count)
        var result = [Int]()
        var tempLists = lists
        
        //First step - Filled minHeap
        for i in 0..<lists.count {
            var list = tempLists[i]
            if !list.isEmpty {
                let minValue = tempLists[i].removeFirst()
                minHeap.insert(value: Element(listIndex: i, value: minValue))
            }
        }
        
        while result.count < total {
            guard let minElement = minHeap.removeHighestPriority() else {
                break
            }
            result.append(minElement.value)
            if !tempLists[minElement.listIndex].isEmpty {
                let nextInsertElement = tempLists[minElement.listIndex].removeFirst()
                minHeap.insert(value: Element(listIndex: minElement.listIndex, value: nextInsertElement))
            }
        }
        print("K Merged Sorted Array: \(result)")
    }
    
    
    mergeKSortedList(total: 13, lists: [
        [5, 6, 7, 9, 10],
        [2, 3, 13, 15, 17, 20],
        [1, 8]
    ])
    
    screenshot 2024 04 05 at 6.37.37e280afpm

    Exercise 4. Find the median in a stream of elements

    The stream implies that we have no idea which elements are going in to come in next – We have to keep track of the median as the elements stream in

    Use a Max Heap to store the smaller (First) Half of elements seen in the stream

    Use a Min Heap to store the larger (Second) Half of elements seen in the stream

    Now if the size of these heaps are such that they differ by no more than 1 element.

    Then the minimum element of the min heap and the maximum element of the maximum heap are the middle elements of the stream

    Getting the median is then a simple calculation

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8474108#content

    screenshot 2024 04 05 at 7.18.16e280afpm
    screenshot 2024 04 05 at 7.18.35e280afpm
    screenshot 2024 04 05 at 7.18.58e280afpm

    Heap Implementation

    enum HeapType {
        case min
        case max
    }
    
    protocol Heap {
        associatedtype Item: Comparable
        var array: [Item?] { get set }
        var count: Int { get set }
        var maxSize: Int? { get set }
        var endIndex: Int { get }
        
        init(maxSize: Int?, type: HeapType)
        
        func leftChildIndex(index: Int, endIndex: Int) -> Int?
        func rightChildIndex(index: Int, endIndex: Int) -> Int?
        func parentIndex(index: Int, endIndex: Int) -> Int?
        func siftDown(index: Int)
        func siftUp(index: Int)
        func insert(value: Item)
        func removeHighestPriority() -> Item?
        func highestPriority() -> Item?
    }
    
    extension Heap {
        func leftChildIndex(index: Int, endIndex: Int) -> Int? {
            let leftIndex = 2 * index + 1
            if leftIndex >= endIndex {
                return nil
            }
            return leftIndex
        }
        
        func rightChildIndex(index: Int, endIndex: Int) -> Int? {
            let rightIndex = 2 * index + 2
            if rightIndex >= endIndex {
                return nil
            }
            return rightIndex
        }
        
        func parentIndex(index: Int, endIndex: Int) -> Int? {
            if index < 0 || index >= endIndex {
                return nil
            }
            return (index - 1) / 2
        }
        
        //MARK: Helper functions
        var isFull: Bool {
            count == array.count
        }
        
        func highestPriority() -> Item? {
            guard count > 0, let firstItem = array.first else {
                return nil
            }
            return firstItem
        }
    }
    class HeapStructure<T: Comparable>: Heap {
        var array: [T?]
        var count: Int
        var maxSize: Int?
        private let type: HeapType
        
        required init(maxSize: Int?, type: HeapType) {
            if let maxSize {
                self.array = Array(repeating: nil, count: maxSize)
            }
            else {
                self.array = [T?]()
            }
            self.type = type
            self.count = 0
            self.maxSize = maxSize
        }
        
        var endIndex: Int {
            if let maxSize {
                return min(count, maxSize)
            }
            return count
        }
        
        //Recursive call
        func siftDown(index: Int) {
            let leftChildIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightChildIndex = rightChildIndex(index: index, endIndex: endIndex)
            //smaller value in chids
            var swapCandidateIndex: Int?
            if leftChildIndex != nil, rightChildIndex != nil {
                switch type {
                case .min:
                    if array[leftChildIndex!]! < array[rightChildIndex!]! {
                        swapCandidateIndex = leftChildIndex
                    }
                    else {
                        swapCandidateIndex = rightChildIndex
                    }
                case .max:
                    if array[leftChildIndex!]! > array[rightChildIndex!]! {
                        swapCandidateIndex = leftChildIndex
                    }
                    else {
                        swapCandidateIndex = rightChildIndex
                    }
                }
            }
            else if leftChildIndex != nil {
                swapCandidateIndex = leftChildIndex
            }
            else if rightChildIndex != nil {
                swapCandidateIndex = rightChildIndex
            }
            //Base case
            guard let swapCandidateIndex, let swapCandidateValue = array[swapCandidateIndex], let currentIndexValue = array[index] else {
                return
            }
            
            switch type {
            case .min:
                if currentIndexValue > swapCandidateValue {
                    let value = array[swapCandidateIndex]
                    //Sift down large value to the next level
                    array[swapCandidateIndex] = currentIndexValue
                    array[index] = swapCandidateValue
                    
                    //Recursive Call
                    siftDown(index: swapCandidateIndex)
                }
            case .max:
                if currentIndexValue < swapCandidateValue {
                    let value = array[swapCandidateIndex]
                    //Sift down large value to the next level
                    array[swapCandidateIndex] = currentIndexValue
                    array[index] = swapCandidateValue
                    
                    //Recursive Call
                    siftDown(index: swapCandidateIndex)
                }
            }
        }
        
        func siftUp(index: Int) {
            guard let parentIndex = parentIndex(index: index, endIndex: endIndex), let currentValue = array[index] else {
                return
            }
            switch type {
            case .min:
                if array[parentIndex]! > currentValue {
                    let parentValue = array[parentIndex]!
                    array[parentIndex] = currentValue
                    array[index] = parentValue
                    siftUp(index: parentIndex)
                }
            case .max:
                if array[parentIndex]! < currentValue {
                    let parentValue = array[parentIndex]!
                    array[parentIndex] = currentValue
                    array[index] = parentValue
                    siftUp(index: parentIndex)
                }
            }
        }
        
        func insert(value: Item) {
            if let maxSize {
                array[count] = value
                let insertedIndex = count
                count += 1
                siftUp(index: insertedIndex)
            }
            else {
                array.append(value)
                let insertedIndex = array.count - 1
                count += 1
                siftUp(index: insertedIndex)
            }
        }
        
        
        func removeHighestPriority() -> T? {
            let root = array[0]
            //Remove First elements
            array.removeFirst()
            count -= 1
            array.swapAt(0, count - 1)
            siftDown(index: 0)
            return root
        }
        
        //Heapify the entire array
        func heapify(index: Int) {
            //Start with the parent index of the last element
            guard var parentIndex = parentIndex(index: index, endIndex: index) else {
                return
            }
            while parentIndex >= 0 {
                percolateDown(index: parentIndex, endIndex: index)
                parentIndex -= 1
            }
        }
        
        func percolateDown(index: Int, endIndex: Int) {
            let leftIndex = leftChildIndex(index: index, endIndex: endIndex)
            let rightIndex = rightChildIndex(index: index, endIndex: endIndex)
            
            switch type {
            case .min:
                //Check If the Min heap property is satisfied
                if let leftIndex, array[leftIndex]! < array[index]! {
                    let currentValue = array[index]!
                    let leftChildValue = array[leftIndex]!
                    array[index] = leftChildValue
                    array[leftIndex] = currentValue
                    percolateDown(index: leftIndex, endIndex: endIndex)
                }
                if let rightIndex, array[rightIndex]! < array[index]! {
                    let currentValue = array[index]!
                    let rightChildValue = array[rightIndex]!
                    array[index] = rightChildValue
                    array[rightIndex] = currentValue
                    percolateDown(index: rightIndex, endIndex: endIndex)
                }
            case .max:
                //Check If the Max heap property is satisfied
                if let leftIndex, array[leftIndex]! > array[index]! {
                    let currentValue = array[index]!
                    let leftChildValue = array[leftIndex]!
                    array[index] = leftChildValue
                    array[leftIndex] = currentValue
                    percolateDown(index: leftIndex, endIndex: endIndex)
                }
                if let rightIndex, array[rightIndex]! > array[index]! {
                    let currentValue = array[index]!
                    let rightChildValue = array[rightIndex]!
                    array[index] = rightChildValue
                    array[rightIndex] = currentValue
                    percolateDown(index: rightIndex, endIndex: endIndex)
                }
            }
        }
    }
    let minHeap = HeapStructure<Int>(maxSize: nil, type: .min)
    let maxHeap = HeapStructure<Int>(maxSize: nil, type: .max)
    
    func getMedianValue(input: Int) -> Int {
        if maxHeap.count != 0, let maxValue = maxHeap.highestPriority(), input > maxValue {
            minHeap.insert(value: input)
            print("Insert \(input) > \(maxValue) to MinHeap")
        }
        else {
            maxHeap.insert(value: input)
            print("Insert \(input) to MaxHeap")
        }
        
        //Check rebalance - Difference is greater 1
        if maxHeap.count > minHeap.count, maxHeap.count - minHeap.count > 1, let _ = maxHeap.highestPriority() {
            let removedMaxValue = maxHeap.removeHighestPriority()!
            minHeap.insert(value: removedMaxValue)
            print("✅ Rebalanced - Insert removed: \(removedMaxValue) to MinHeap")
        }
        else if minHeap.count > maxHeap.count, minHeap.count - maxHeap.count > 1, let minValue = minHeap.highestPriority() {
            let removedMinValue = minHeap.removeHighestPriority()!
            maxHeap.insert(value: removedMinValue)
            
            print("✅ Rebalanced - Insert removed: \(removedMinValue) to MaxHeap")
        }
        
        if minHeap.count == maxHeap.count {
            return (minHeap.highestPriority()! + maxHeap.highestPriority()!) / 2
        }
        
        return minHeap.count > maxHeap.count ? minHeap.highestPriority()! : maxHeap.highestPriority()!
    }
    
    let input = [5, 6, 7, 9, 10, 2, 3, 13, 15, 17, 20, 1, 8]
    
    
    for i in input {
        getMedianValue(input: i)
    }
    
    print("MinHeap: \(minHeap.array)")
    print("MaxHeap: \(maxHeap.array)")
    screenshot 2024 04 05 at 9.22.01e280afpm

    Tip: Real Interview using Swift

    Heap data structure is not part of Foundation. It means you need to implement heap if you want to solve Heap related problems.

    It’s not easy. Implementing Heap in 1 hour interview is not make sense. You can see Apple’s official Heap data structure

    screenshot 2024 04 06 at 12.28.12e280afam

    You can use array and sort items when insert value. Let’s call it FakeHeap!

    FakeHeap source code

    class FakeMinHeap {
        var array: [Int?]
        let maxSize: Int?
        init(maxSize: Int?) {
            self.maxSize = maxSize
            if let maxSize {
                array = Array(repeating: nil, count: maxSize)
            }
            else {
                array = [Int?]()
            }
        }
        
        var count: Int {
            array.count
        }
        
        func insert(value: Int) {
            array.append(value)
            array = array.compactMap { $0 }.sorted(by: <)
        }
        
        func highestPriority() -> Int? {
            return array.first ?? nil
        }
        
        func removeHighestPriority() -> Int? {
            return array.removeFirst()
        }
    }
    
    class FakeMaxHeap {
        var array: [Int?]
        let maxSize: Int?
        init(maxSize: Int?) {
            self.maxSize = maxSize
            if let maxSize {
                array = Array(repeating: nil, count: maxSize)
            }
            else {
                array = [Int?]()
            }
        }
        
        var count: Int {
            array.count
        }
        
        func insert(value: Int) {
            array.append(value)
            array = array.compactMap { $0 }.sorted(by: >)
        }
        
        func highestPriority() -> Int? {
            return array.first ?? nil
        }
        
        func removeHighestPriority() -> Int? {
            return array.removeFirst()
        }
    }
    
    
    let minHeap = FakeMinHeap(maxSize: nil)
    let maxHeap = FakeMaxHeap(maxSize: nil)

    Let’s test last exercise. Find the median in a stream of elements using FakeHeap!

    let minHeap = FakeMinHeap(maxSize: nil)
    let maxHeap = FakeMaxHeap(maxSize: nil)
    
    func getMedianValue(input: Int) -> Int {
        if maxHeap.count != 0, let maxValue = maxHeap.highestPriority(), input > maxValue {
            minHeap.insert(value: input)
            print("Insert \(input) > \(maxValue) to MinHeap")
        }
        else {
            maxHeap.insert(value: input)
            print("Insert \(input) to MaxHeap")
        }
        
        //Check rebalance - Difference is greater 1
        if maxHeap.count > minHeap.count, maxHeap.count - minHeap.count > 1, let _ = maxHeap.highestPriority() {
            let removedMaxValue = maxHeap.removeHighestPriority()!
            minHeap.insert(value: removedMaxValue)
            print("✅ Rebalanced - Insert removed: \(removedMaxValue) to MinHeap")
        }
        else if minHeap.count > maxHeap.count, minHeap.count - maxHeap.count > 1, let minValue = minHeap.highestPriority() {
            let removedMinValue = minHeap.removeHighestPriority()!
            maxHeap.insert(value: removedMinValue)
            
            print("✅ Rebalanced - Insert removed: \(removedMinValue) to MaxHeap")
        }
        
        if minHeap.count == maxHeap.count {
            return (minHeap.highestPriority()! + maxHeap.highestPriority()!) / 2
        }
        
        return minHeap.count > maxHeap.count ? minHeap.highestPriority()! : maxHeap.highestPriority()!
    }
    
    let input = [5, 6, 7, 9, 10, 2, 3, 13, 15, 17, 20, 1, 8]
    
    
    for i in input {
        getMedianValue(input: i)
    }
    
    print("MinHeap: \(minHeap.array)")
    print("MaxHeap: \(maxHeap.array)")
    screenshot 2024 04 05 at 9.39.50e280afpm

    As you can see, result is the same as Real Heap. (Absolutely time complexity is different compare with real heap data structure, because It’s fake heap to solve problem during the interview)

  • iOS, Autorelease Pool

    iOS, Autorelease Pool

    ✍️ Note

    Some codes and contents are sourced from Apple’s official documentation. This post is for personal notes where I summarize the original contents to grasp the key concepts

    What is Autorelease Pool?

    An object that supports Cocoa’s reference-counted memory management system.

    An autorelease pool stores objects that are sent a release message when the pool itself is drained.

    If you use Automatic Reference Counting (ARC), you cannot use autorelease pools directly. Instead, you use @autoreleasepool blocks. 

    @autoreleasepool blocks are more efficient than using an instance of NSAutoreleasePool directly; you can also use them even if you do not use ARC.

    https://developer.apple.com/documentation/foundation/nsautoreleasepool

    screenshot 2024 04 03 at 11.54.58e280afam

    AutoRelease Pool

    You can send multiple autorelease message to an object. When autorelease pool released, object will receive multiple release message.

    screenshot 2024 04 03 at 12.21.10e280afpm
    screenshot 2024 04 03 at 12.34.59e280afpm

    Common Scenario when Is it useful for? -> Created an object in a Loop!

    for (i=0; i < MANY_TIMES; i++)
    {
      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    //Create an object and send autorelease message
    
    MyObject *object = [[MyObject alloc] init];
    [object autorelease];
    
    //Release all objects that created in this loop.
    [pool release];
    
    }
  • iOS, File System

    iOS, File System

    ✍️ Note

    Some codes and contents are sourced from Apple’s official documentation. This post is for personal notes where I summarize the original contents to grasp the key concepts (🎨 some images I draw it)

    About the iOS File System

    For security purposes, an iOS app’s interactions with the file system are limited to the directories inside the app’s sandbox directory. During installation of a new app, the installer creates a number of container directories for the app inside the sandbox directory. Each container directory has a specific role. The bundle container directory holds the app’s bundle, whereas the data container directory holds data for both the app and the user. The data container directory is further divided into a number of subdirectories that the app can use to sort and organize its data. The app may also request access to additional container directories—for example, the iCloud container—at runtime. 

    These container directories constitute the app’s primary view of the file system. Figure 1-1 shows a representation of the sandbox directory for an app.

    An app is generally prohibited from accessing or creating files outside its container directories. One exception to this rule is when an app uses public system interfaces to access things such as the user’s contacts or music. In those cases, the system frameworks use helper apps to handle any file-related operations needed to read from or modify the appropriate data stores.

    https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

    screenshot 2024 03 31 at 5.39.37e280afpm
    screenshot 2024 03 31 at 5.52.42e280afpm

    Where You Should Put Your App’s Files

    To prevent the syncing and backup processes on iOS devices from taking a long time, be selective about where you place files. Apps that store large files can slow down the process of backing up to iTunes or iCloud. These apps can also consume a large amount of a user’s available storage, which may encourage the user to delete the app or disable backup of that app’s data to iCloud. With this in mind, you should store app data according to the following guidelines:

    • Put user data in Documents/. User data generally includes any files you might want to expose to the user—anything you might want the user to create, import, delete or edit. For a drawing app, user data includes any graphic files the user might create. For a text editor, it includes the text files. Video and audio apps may even include files that the user has downloaded to watch or listen to later.
    • Put app-created support files in the Library/Application support/ directory. In general, this directory includes files that the app uses to run but that should remain hidden from the user. This directory can also include data files, configuration files, templates and modified versions of resources loaded from the app bundle.
    • Remember that files in Documents/ and Application Support/ are backed up by default. You can exclude files from the backup by calling -[NSURL setResourceValue:forKey:error:] using the NSURLIsExcludedFromBackupKey key. Any file that can be re-created or downloaded must be excluded from the backup. This is particularly important for large media files. If your application downloads video or audio files, make sure they are not included in the backup.
    • Put temporary data in the tmp/ directory. Temporary data comprises any data that you do not need to persist for an extended period of time. Remember to delete those files when you are done with them so that they do not continue to consume space on the user’s device. The system will periodically purge these files when your app is not running; therefore, you cannot rely on these files persisting after your app terminates.
    • Put data cache files in the Library/Caches/ directory. Cache data can be used for any data that needs to persist longer than temporary data, but not as long as a support file. Generally speaking, the application does not require cache data to operate properly, but it can use cache data to improve performance. Examples of cache data include (but are not limited to) database cache files and transient, downloadable content. Note that the system may delete the Caches/ directory to free up disk space, so your app must be able to re-create or download these files as needed.

    https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

    Library/Preferences Directory

    This directory contains app-specific preference files. You should not create files in this directory yourself. Instead, use the NSUserDefaults class or CFPreferences API to get and set preference values for your app. In iOS, the contents of this directory are backed up by iTunes and iCloud.
    Apple

    https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

    The iCloud File Storage Container

    iCloud provides a structured system for storing files for apps that make use of iCloud:

    • Apps have a primary iCloud container directory for storing their native files.  They can also access secondary iCloud container directories listed in their app entitlements.
    • Inside each container directory, files are segregated into “documents” and data.  Every file or file package located in the Documents subdirectory (or one of its subdirectories) is presented to the user (via the iCloud UI in macOS and iOS) as a separate document that can be deleted individually.   Anything not in Documents or one of its subdirectories is treated as data and shown as a single entry in the iCloud UI.

    Documents that the user creates and sees in an app’s user interface—for example the document browsers in Pages, Numbers, and Keynote should be stored in the Documents directory. Another example of files that might go in the Documents directory are saved games, again because they are something that an app could potentially provide some sort of method for selecting.

    Anything that the app does not want the user to see or modify directly should be placed outside of the Documents directory.  Apps can create any subdirectories inside the container directory, so they can arrange private files as desired.

    Apps create files and directories in iCloud container directories in exactly the same way as they create local files and directories.  And all the file’s attributes are saved, if they add extended attributes to a file, those attributes are copied to iCloud and to the user’s other devices too. 

    iCloud containers also allow the storage of key-value pairs that can be easily accessed without having to create a document format.

    https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

    Security: Protect the Files You Create

    Sandboxes prevent apps from writing to parts of the file system that they should not write to. Each sandboxed app receives one or more containers that it can write into. An app cannot write to other apps’ containers or to most directories outside of the sandbox. These restrictions limit the potential damage that can be done in the event that an app’s security is breached.

    iOS apps always run in a sandbox, the system assigns specific ACLs and permissions to files created by each app.

    https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

    Files Can Be Encrypted On Disk

    iOS. An iOS app can designate files that it wants to be encrypted on disk. When the user unlocks a device containing encrypted files, the system creates a decryption key that allows the app to access its encrypted files. When the user locks the device, though, the decryption key is destroyed to prevent unauthorized access to the files.

    in iOS, apps that take advantage of disk-based encryption need to be discontinue the use of encrypted files when the user locks the device. Because locking the device destroys the decryption keys, access to encrypted files is limited to when the device is unlocked. If your iOS app can run in the background while the device is locked, it must do so without access to any of its encrypted files. Because encrypted disks in macOS are always accessible while the computer is running, macOS apps do not need to do anything special to handle disk-level encryption.

    https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

    Files, Concurrency, and Thread Safety

    Because file-related operations involve interacting with the hard disk and are therefore slow compared to most other operations, most of the file-related interfaces in iOS and macOS are designed with concurrency in mind. Several technologies incorporate asynchronous operation into their design and most others can execute safely from a dispatch queue or secondary thread. Table 1-4 lists some of the key technologies discussed in this document and whether they are safe to use from specific threads or any thread. For specific information about the capabilities of any interface, see the reference documentation for that interface.

    Even if you use an thread-safe interface for manipulating a file, problems can still arise when multiple threads or multiple processes attempt to act on the same file. Although there are safeguards to prevent multiple clients from modifying a file at the same time, those safeguards do not always guarantee exclusive access to the file at all times. (Nor should you attempt to prevent other processes from accessing shared files.) To make sure your code knows about changes made to shared files, use file coordinators to manage access to those files. For more information about file coordinators, see The Role of File Coordinators and Presenters

    https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

    screenshot 2024 03 31 at 7.45.50e280afpm
  • iOS, Stack and Queue

    iOS, Stack and Queue

    ✍️ Note

    Some codes and contents are sourced from Udemy. This post is for personal notes where I summarize the original contents to grasp the key concepts (🎨 some images I draw it)

    Introduction

    You may think Stack and Queue is very basic structure and It’s very easy. Yes that’s true but utilizing them together is bit tricky. That’s why I summarized these data structure. (As I mentioned earlier, all the contents are sourced from Udemy lecture, I rewrite sample code in Swift)

    Stack

    A stack is a data structure to hold elements such that the last element you add to the stack is the first one you access

    LIFO: Last In First Out

    Always focused on the end of the stack, called the POP

    Push – Push an element to the top of a stack

    Pop – Pop an element from the top of a stack

    Peek – Peet at the top element of a stack (Don’t remove an element!) You access the top element in the stack without actually changing the data structure

    Other operations which are useful

    IsEmpty, IsFull, Size

    Exception case

    • When you try to pop from an empty stack -> throw an exception
    • Push into a full stack -> throw an exception, Yes It is stack overflow!

    Summary

    • The most common operations on a stack involve pushing and popping elements from the top.
    • The operations are confined to one end of the stack
    • A linked list lends itself perfectly to build a stack

    Performance and Complexity

    • Push and Pop (Using Linked List or Array) is O(1)
    • isEmpty, isFull also O(1)
    • check size -> O(1)

    Space Complexity

    • O(N)

    Where can stacks be used?

    • Implementing undo in an application
    • Implementing the back button on the web browser
    • Holding the memory for recursive calls in a programming language
    • Translating infix notation (a+b) for expressions to postfix (ab+)
      • programming languages converts infix to postfix

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463684#overview

    screenshot 2024 03 31 at 11.54.27e280afam

    Example 1. Use a Single Linked List

    //A Single Element in the linked list
    class Element<T> {
        var data: T
        var next: Element?
        
        init(data: T, next: Element?) {
            self.data = data
            self.next = next
        }
    }
    
    enum StackError: Error {
        //Pushing into a full stack
        case stackOverflow
        //Popping from or peeking into an empty stack
        case stackUnderflow
    }
    
    class Stack<T> {
        private let maxSize: Int
        //Topmost element in the stack
        private var top: Element<T>?
        //Track the size of the stack at every push, pop
        //This means the stack size operation can be constant time
        private var size = 0
        
        init(maxSize: Int) {
            self.maxSize = maxSize
        }
        
        func push(data: T) throws {
            if size == maxSize {
                throw StackError.stackOverflow
            }
            //Create a new element to hold the data and point top to it. Make sure you increment the size of the stack
            var newElement = Element(data: data, next: top)
            top = newElement
            size += 1
        }
        
        func pop() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            guard let topElement = top else {
                throw StackError.stackUnderflow
            }
            let data = topElement.data
            //Remove the top element from the stack and return it's data
            top = topElement.next
            size -= 1
            return data
        }
        
        func peek() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            guard let topElement = top else {
                throw StackError.stackUnderflow
            }
            //Just return the data of the top element, do not remove it
            return topElement.data
        }
        
        var isEmpty: Bool {
            size == 0
        }
        
        var isFull: Bool {
            size == maxSize
        }
    }
    
    screenshot 2024 03 31 at 10.39.59e280afam

    Example 2. Use an Array (It’s more simple, In swift array can increased the size at runtime)

    Growing the Size of an Array

    Every array reserves a specific amount of memory to hold its contents. When you add elements to an array and that array begins to exceed its reserved capacity, the array allocates a larger region of memory and copies its elements into the new storage. The new storage is a multiple of the old storage’s size. This exponential growth strategy means that appending an element happens in constant time, averaging the performance of many append operations. Append operations that trigger reallocation have a performance cost, but they occur less and less often as the array grows larger.

    If you know approximately how many elements you will need to store, use the reserveCapacity(_:)method before appending to the array to avoid intermediate reallocations. Use the capacity and countproperties to determine how many more elements the array can store without allocating larger storage.

    For arrays of most Element types, this storage is a contiguous block of memory. For arrays with an Element type that is a class or @objc protocol type, this storage can be a contiguous block of memory or an instance of NSArray. Because any arbitrary subclass of NSArray can become an Array, there are no guarantees about representation or efficiency in this case.

    https://developer.apple.com/documentation/swift/array

    enum StackError: Error {
        //Pushing into a full stack
        case stackOverflow
        //Popping from or peeking into an empty stack
        case stackUnderflow
    }
    
    class Stack<T> {
        private let maxSize: Int
        private(set) var array = Array<T>()
        //Topmost element in the stack
        private var top: T?
        //Track the size of the stack at every push, pop
        //This means the stack size operation can be constant time
        private var size = 0
        
        init(maxSize: Int) {
            self.maxSize = maxSize
        }
        
        func push(data: T) throws {
            if size == maxSize {
                throw StackError.stackOverflow
            }
            array.append(data)
            top = data
            size += 1
        }
        
        func pop() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            //Remove the top element from the stack and return it's data
            let topElement = array.removeLast()
            top = array.last
            size -= 1
            return topElement
        }
        
        func peek() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            guard let topElement = top else {
                throw StackError.stackUnderflow
            }
            //Just return the data of the top element, do not remove it
            return topElement
        }
        
        var isEmpty: Bool {
            size == 0
        }
        
        var isFull: Bool {
            size == maxSize
        }
    }
    
    let stack = Stack<Int>(maxSize: 10)
    
    try? stack.push(data: 1)
    try? stack.push(data: 2)
    try? stack.push(data: 3)
    
    try? stack.pop()
    try? stack.pop()
    try? stack.pop()
    screenshot 2024 03 31 at 10.47.06e280afam

    Exercise 1. Match parenthesis in an expression

    Check whether an expression has well-formed parenthesis I.E The opening and the closing brackets match

    Use a Stack to store the opening brackets each time you encounter one

    For every closing bracket compare to the last element pushed onto the stack

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463688#overview

    Example

    • (ABC){DEF}[XYZ(LMN)] -> Match
    • (ABC{DEF}[XYZ(LMN)] -> Mismatch
    • (ABC){DEF}[XYZ(LMN)]} -> Mismatch
    screenshot 2024 03 31 at 11.12.30e280afam
    //Key is closed brackets, value is open brackets
    var brackets = [")": "(", "]": "[", "}": "{"]
    var openBrackets = Set(brackets.values)
    
    print(openBrackets)
    func matchingParenthesis(input: String) -> Bool {
        let stack = Stack<String>(maxSize: input.count)
        var inputStringArray = input.compactMap { String($0)}
        for c in inputStringArray {
            if openBrackets.contains(c) {
                try? stack.push(data: c)
            }
            //Check close bracket
            if brackets.keys.contains(c) {
                //Pop open brackets
                let poppedValue = try? stack.pop()
                //Check Is poppedValue openBracket or not
                if poppedValue != brackets[c] {
                    return false
                }
            }
        }
        return stack.isEmpty
    }
    
    matchingParenthesis(input: "(ABC){DEF}[XYZ(LMN)]")
    matchingParenthesis(input: "(ABC{DEF}[XYZ(LMN)]")
    matchingParenthesis(input: "(ABC){DEF}[XYZ(LMN)]}")
    screenshot 2024 03 31 at 11.30.50e280afam

    Exercise 2. Find the minimum element in a stack in constant time

    Keep track of the minimum element for each element pushed on to the stack

    Have 2 stacks, one to hold the actual elements, and another to hold the minimum element corresponding to every element added to the stack

    When each element is pushed onto the stack compare it with the last minimum element and push both the main stack and the minimum element stack together

    Do the same when popping an element from the stack

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463690#overview

    screenshot 2024 03 31 at 2.05.27e280afpm
    screenshot 2024 03 31 at 2.21.04e280afpm
    func findMinValue(input: [Int]) -> Int? {
        let stack = Stack<Int>(maxSize: input.count)
        let minStack = Stack<Int>(maxSize: input.count)
        
        for n in input {
            try? stack.push(data: n)
            if minStack.isEmpty == true {
                try? minStack.push(data: n)
            }
            else {
                if let peekMinValue = try? minStack.peek() {
                    //Compare peekValue from MinStack and current value in input
                    let currentMinValue = n < peekMinValue ? n : peekMinValue
                    try? minStack.push(data: currentMinValue)
                }
            }
        }
        return try? minStack.peek()
    }
    
    findMinValue(input: [4, 5, 3, 2, 9])
    screenshot 2024 03 31 at 2.16.58e280afpm

    Or you can create a MinimumStack class like below

    enum StackError: Error {
        //Pushing into a full stack
        case stackOverflow
        //Popping from or peeking into an empty stack
        case stackUnderflow
    }
    
    class Stack<T> {
        private let maxSize: Int
        private(set) var array = Array<T>()
        //Topmost element in the stack
        private var top: T?
        //Track the size of the stack at every push, pop
        //This means the stack size operation can be constant time
        private var size = 0
        
        init(maxSize: Int) {
            self.maxSize = maxSize
        }
        
        func push(data: T) throws {
            if size == maxSize {
                throw StackError.stackOverflow
            }
            array.append(data)
            top = data
            size += 1
        }
        
        func pop() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            //Remove the top element from the stack and return it's data
            let topElement = array.removeLast()
            top = array.last
            size -= 1
            return topElement
        }
        
        func peek() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            guard let topElement = top else {
                throw StackError.stackUnderflow
            }
            //Just return the data of the top element, do not remove it
            return topElement
        }
        
        var isEmpty: Bool {
            size == 0
        }
        
        var isFull: Bool {
            size == maxSize
        }
    }
    
    
    class MinimumStack<T: Comparable> {
        private let maxSize: Int
        private let stack: Stack<T>
        private let minimumStack: Stack<T>
        
        init(maxSize: Int) {
            self.maxSize = maxSize
            self.stack = Stack<T>(maxSize: maxSize)
            self.minimumStack = Stack<T>(maxSize: maxSize)
        }
        
        func push(data: T) throws {
            var min = data
            if !minimumStack.isEmpty {
                let peekMinValue = try minimumStack.peek()
                if min > peekMinValue {
                    min = peekMinValue
                }
            }
            try stack.push(data: data)
            try minimumStack.push(data: min)
        }
        
        func pop() throws -> T {
            try minimumStack.pop()
            return try stack.pop()
        }
        
        func minimumValue() throws -> T {
            return try minimumStack.peek()
        }
    }
    
    let input = [4, 5, 3, 2, 9]
    let minimumStack = MinimumStack<Int>(maxSize: input.count)
    for n in input {
        try? minimumStack.push(data: n)
    }
    try? minimumStack.minimumValue()
    
    screenshot 2024 03 31 at 2.31.00e280afpm

    Queue

    A queue is a data structure where you add elements to the end of the queue and remove elements from the beginning of the queue

    FIFO: First In First Out == LILO: List In Last Out

    The operations on a Queue are performed at two ends, removal is at the beginning and addition is at the end of the Queue.

    Operations

    • Enqueue
      • Adding a new element to the end of the queue is called
      • Enqueue an element from the queue
    • Dequeue
      • Removing an element from the beginning of a queue is called
      • Dequeue an element from the queue
    • Peek
      • Similar to a stack you might just want to see what the first element in a queue is without removing it
      • Peek at the first element in a queue
    • Offer
      • The queue implementation in Java has another useful method
      • Add to a queue if space is available

    Edge case

    • What if you try to dequeue from an empty queue?
    • Or Enqueue into a full queue?
    • -> Throw an exception!

    Underlying Data Structure

    • Linked List is commonly used
    • The most common operations on a queue involve enqueuing and dequeuing elements
    • The operations are on both ends of the queue
    • A linked list with a pointer to head and the tail works well
    • A common data structure used is a circular queue with pointers to the head and to the tail

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463696#overview

    Enqueue

    screenshot 2024 03 31 at 3.18.52e280afpm

    Dequeue

    screenshot 2024 03 31 at 3.21.05e280afpm

    Exercise 1. Circular Queue

    It can be implemented using an array where the last element wraps around to the first element

    Implementation

    • Head = -1
      • A special value to denote an empty list
    • Tail
      • Always indicates last index

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463698#overview

    Enqueue datas until It is full

    screenshot 2024 03 31 at 3.43.25e280afpm

    Dequeue n items

    screenshot 2024 03 31 at 3.49.13e280afpm
    enum QueueError: Error {
        case queueOverflow
        case queueUnderflow
    }
    
    class Queue {
        private let specialEmptyValue = -1
        private let size: Int
        private var items = Array<Int?>()
        
        
        //Initialize both the head and the tail indices to the special value
        private var headIndex: Int
        private var tailIndex: Int
        
        init(size: Int) {
            headIndex = specialEmptyValue
            tailIndex = specialEmptyValue
            self.size = size
            //Create an array with fixed size
            self.items = Array(repeating: nil, count: size)
        }
        
        //Enqueue doesn't affect headIndex except for empty case
        func enqueue(data: Int) throws {
            if isFull {
                throw QueueError.queueOverflow
            }
            //Get the next tail index and insert the new element there
            tailIndex = (tailIndex + 1) % items.count
            items[tailIndex] = data
            
            //This is the first element enqueued, set the head index to the tail index
            if headIndex == specialEmptyValue {
                headIndex = tailIndex
            }
        }
        
        func dequeue() throws -> Int {
            if isEmpty {
                throw QueueError.queueUnderflow
            }
            guard let firstElement = items[headIndex] else {
                throw QueueError.queueUnderflow
            }
            //Edge case - This element is the last item in a queue (It means there is only one item in a queue)
            if headIndex == tailIndex {
                headIndex = specialEmptyValue
            }
            else {
                headIndex = (headIndex + 1) % items.count
            }
            return firstElement
        }
        
        //Headindex is always set to special marker value when the queue is empty
        var isEmpty: Bool {
            headIndex == specialEmptyValue
        }
        
        //WHen the queue is full this means that the head index and tail index are right next to one another
        var isFull: Bool {
            let nextIndex = (tailIndex + 1) % items.count
            //Check whether the next position of the tail is the head index
            return nextIndex == headIndex
        }
    }
    
    screenshot 2024 03 31 at 4.21.54e280afpm

    The Queue – Performance and Complexity

    • Enqueuing and Dequeuing implemented in this way is O(1)
    • IsEmpty and IsFull is also O(1)
    • Space complexity is O(N)

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463698#overview

    Exercise 2. Build a Queue with Two Stack

    Where can a queue be used?

    • Customer service hotline, calls are assigned to representatives in the order that they are received
    • Queueing jobs to be printed
    • Any order processing systems like in e-commerce websites or bank transaction systems

    Implement a Queue using two stacks

    • Forward Stack
      • It used to push the enqueued elements
      • Enqueue operations are always performed on the forward stack
      • Enqueue requires moving all elements to the forward stack and pushing the last element in I.E. The top
    • Reverse Stack
      • Holds the elements in reverse order from the forward stack
      • Dequeue operations are always performed on the reverse stack
      • Dequeue requires moving all elements to the reverse track and popping the last element in I.E the Top

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463700#overview

    screenshot 2024 03 31 at 4.52.37e280afpm
    screenshot 2024 03 31 at 4.53.09e280afpm
    screenshot 2024 03 31 at 4.53.27e280afpm

    So It looks tricky. It has 2 stacks to implement Enqueue and Dequeue operation like a Queue.

    enum StackError: Error {
        //Pushing into a full stack
        case stackOverflow
        //Popping from or peeking into an empty stack
        case stackUnderflow
    }
    
    class Stack<T> {
        private let maxSize: Int
        private(set) var array = Array<T>()
        //Topmost element in the stack
        private var top: T?
        //Track the size of the stack at every push, pop
        //This means the stack size operation can be constant time
        private var size = 0
        
        init(maxSize: Int) {
            self.maxSize = maxSize
        }
        
        func push(data: T) throws {
            if size == maxSize {
                throw StackError.stackOverflow
            }
            array.append(data)
            top = data
            size += 1
        }
        
        func pop() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            //Remove the top element from the stack and return it's data
            let topElement = array.removeLast()
            top = array.last
            size -= 1
            return topElement
        }
        
        func peek() throws -> T {
            if size == 0 {
                throw StackError.stackUnderflow
            }
            guard let topElement = top else {
                throw StackError.stackUnderflow
            }
            //Just return the data of the top element, do not remove it
            return topElement
        }
        
        var isEmpty: Bool {
            size == 0
        }
        
        var isFull: Bool {
            size == maxSize
        }
    }
    
    class QueueUsingTwoStack<T> {
        let forwardStack: Stack<T>
        let reverseStack: Stack<T>
        
        init(size: Int) {
            self.forwardStack = Stack<T>(maxSize: size)
            self.reverseStack =  Stack<T>(maxSize: size)
        }
        
        var isFull: Bool {
            forwardStack.isFull || reverseStack.isFull
        }
        
        var isEmpty: Bool {
            forwardStack.isEmpty && reverseStack.isEmpty
        }
        
        func enqueue(data: T) throws {
            if isFull {
                throw StackError.stackOverflow
            }
            if forwardStack.isEmpty {
                //Push all elements from the reverse stack to the forward stack, enqueue always happens on the forward stack
                while !reverseStack.isEmpty {
                    let popedValue = try reverseStack.pop()
                    try forwardStack.push(data: popedValue)
                }
            }
            //Finally do the enqueue - A push on the forward stack
            try forwardStack.push(data: data)
        }
        
        func dequeue() throws -> T {
            if isEmpty {
                throw StackError.stackUnderflow
            }
            if reverseStack.isEmpty {
                //Push all elements from the forward stack to the reverse stack, dequeue alwyas happens on the reverse stack
                while !forwardStack.isEmpty {
                    let poppedValue = try forwardStack.pop()
                    try reverseStack.push(data: poppedValue)
                }
            }
            //Finally do the dequeue - A pop on the reverse stack
            return try reverseStack.pop()
        }
    }
    
    let queue = QueueUsingTwoStack<Int>(size: 4)
    
    try? queue.enqueue(data: 1)
    try? queue.enqueue(data: 2)
    try? queue.enqueue(data: 3)
    try? queue.enqueue(data: 4)
    queue.isFull
    
    try? queue.dequeue()
    
    screenshot 2024 03 31 at 5.08.28e280afpm

    Performance

    All enqueues and then all dequeues are O(1) – If only one of these operations are performed

    Notice that each element is pushed no more than twice

    • Once onto the forward stack to enqueue it and once onto the reverse stack just before dequeueing

    Each element is popped no more than twice

    • Once to move to the reverse from the forward stack just before dequeuing and then to actually dequeue it

    Time complexity is O(M) where M is the number of operations we perform on the queue.

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8463700#overview

  • iOS, App Architectures

    iOS, App Architectures

    ✍️ Note

    Some codes and contents are sourced from Apple’s official documentation, google android official site and wikipedia. This post is for personal notes where I summarize the original contents to grasp the key concepts (🎨 some images I draw it)

    MVC: Model View Controller

    The Model-View-Controller (MVC) design pattern assigns objects in an application one of three roles: model, view, or controller. The pattern defines not only the roles objects play in the application, it defines the way objects communicate with each other. Each of the three types of objects is separated from the others by abstract boundaries and communicates with objects of the other types across those boundaries. The collection of objects of a certain MVC type in an application is sometimes referred to as a layer—for example, model layer.

    MVC is central to a good design for a Cocoa application. The benefits of adopting this pattern are numerous. Many objects in these applications tend to be more reusable, and their interfaces tend to be better defined. Applications having an MVC design are also more easily extensible than other applications. Moreover, many Cocoa technologies and architectures are based on MVC and require that your custom objects play one of the MVC roles.

    https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

    screenshot 2024 03 30 at 12.07.33e280afpm

    Model Objects

    Model objects encapsulate the data specific to an application and define the logic and computation that manipulate and process that data. For example, a model object might represent a character in a game or a contact in an address book. A model object can have to-one and to-many relationships with other model objects, and so sometimes the model layer of an application effectively is one or more object graphs. Much of the data that is part of the persistent state of the application (whether that persistent state is stored in files or databases) should reside in the model objects after the data is loaded into the application. Because model objects represent knowledge and expertise related to a specific problem domain, they can be reused in similar problem domains. Ideally, a model object should have no explicit connection to the view objects that present its data and allow users to edit that data—it should not be concerned with user-interface and presentation issues. 

    Communication: User actions in the view layer that create or modify data are communicated through a controller object and result in the creation or updating of a model object. When a model object changes (for example, new data is received over a network connection), it notifies a controller object, which updates the appropriate view objects.

    Apple

    View Objects

    A view object is an object in an application that users can see. A view object knows how to draw itself and can respond to user actions. A major purpose of view objects is to display data from the application’s model objects and to enable the editing of that data. Despite this, view objects are typically decoupled from model objects in an MVC application. 

    Because you typically reuse and reconfigure them, view objects provide consistency between applications. Both the UIKit and AppKit frameworks provide collections of view classes, and Interface Builder offers dozens of view objects in its Library. 

    Communication: View objects learn about changes in model data through the application’s controller objects and communicate user-initiated changes—for example, text entered in a text field—through controller objects to an application’s model objects.

    Apple

    Controller Objects

    A controller object acts as an intermediary between one or more of an application’s view objects and one or more of its model objects. Controller objects are thus a conduit through which view objects learn about changes in model objects and vice versa. Controller objects can also perform setup and coordinating tasks for an application and manage the life cycles of other objects. 

    Communication: A controller object interprets user actions made in view objects and communicates new or changed data to the model layer. When model objects change, a controller object communicates that new model data to the view objects so that they can display it.

    Apple

    screenshot 2024 03 30 at 12.27.04e280afpm

    UIViewController

    A view controller’s main responsibilities include the following:

    • Updating the contents of the views, usually in response to changes to the underlying data
    • Responding to user interactions with views
    • Resizing views and managing the layout of the overall interface
    • Coordinating with other objects — including other view controllers — in your app

    https://developer.apple.com/documentation/uikit/uiviewcontroller

    MVP: Model View Presenter

    model view presenter gui design pattern

    Model–View–Presenter

    MVP is a user interface architectural pattern engineered to facilitate automated unit testing and improve the separation of concerns in presentation logic:

    • The model is an interface defining the data to be displayed or otherwise acted upon in the user interface.
    • The view is a passive interface that displays data (the model) and routes user commands (events) to the presenter to act upon that data.
    • The presenter acts upon the model and the view. It retrieves data from repositories (the model), and formats it for display in the view.

    Normally, the view implementation instantiates the concrete presenter object, providing a reference to itself. 

    The degree of logic permitted in the view varies among different implementations. At one extreme, the view is entirely passive, forwarding all interaction operations to the presenter. In this formulation, when a user triggers an event method of the view, it does nothing but invoke a method of the presenter that has no parameters and no return value. The presenter then retrieves data from the view through methods defined by the view interface. Finally, the presenter operates on the model and updates the view with the results of the operation. Other versions of model–view–presenter allow some latitude with respect to which class handles a particular interaction, event, or command. This is often more suitable for web-based architectures, where the view, which executes on a client’s browser, may be the best place to handle a particular interaction or command.

    From a layering point of view, the presenter class might be considered as belonging to the application layer in a multilayered architecture system, but it can also be seen as a presenter layer of its own between the application layer and the user interface layer.

    https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter

    Wikipedia provided an example written in c#

    public class Presenter : IPresenter 
    {
        public Presenter(IView view) 
        {
            // ...
        }
    }
    
    public class View : IView
    {
        private IPresenter _presenter;
    
        public View()
        {
            _presenter = new Presenter(this);
        }
    }

    I wrote an example written in Swift

    screenshot 2024 03 30 at 2.49.03e280afpm
    //View Interface
    protocol AccountViewProtocol: AnyObject {
        var presenter: AccountPresentProtocol! { get set }
        
        //From Presenter
        func display(username: String)
        func showError(message: String)
    }
    
    //Presenter Interface
    protocol AccountPresentProtocol {
        init(view: AccountViewProtocol, service: AccountServiceInterface)
        
        //Actions from ViewProtocol
        func update(username: String)
    }
    
    //Model Interface: Optional
    protocol AccountModelProtocol {
        func usernameIsNotEmpty() -> Bool
        mutating func update(username: String) -> Bool
    }
    
    struct AccountModel: AccountModelProtocol {
        var username: String
        var password: String
        
        func usernameIsNotEmpty() -> Bool {
            !username.isEmpty
        }
        
        mutating func update(username: String) -> Bool {
            guard usernameIsNotEmpty() else {
                return false
            }
            self.username = username
            return true
        }
    }
    
    protocol AccountServiceInterface {
        func getUser() async throws -> AccountModel
    }
    
    class AccountService: AccountServiceInterface {
        func getUser() async throws -> AccountModel {
            AccountModel(username: "Sungwook", password: "1234")
        }
    }
    
    class AccountPresenter: AccountPresentProtocol {
        private weak var view: AccountViewProtocol?
        private var model: AccountModelProtocol?
        private let service: AccountServiceInterface
        
        required init(view: AccountViewProtocol, service: AccountServiceInterface) {
            self.view = view
            self.service = service
        }
        
        func update(username: String) {
            @Sendable func notifyModelUpdateStatus() {
                let isUpdated = model?.update(username: username) ?? false
                isUpdated ? view?.display(username: username) : view?.showError(message: "Username can't be empty")
            }
            
            if model == nil {
                Task { [weak self] in
                    do {
                        let userModel = try await self?.service.getUser()
                        self?.model = userModel
                        notifyModelUpdateStatus()
                    }
                    catch {
                        self?.view?.showError(message: error.localizedDescription)
                    }
                }
            }
            else {
                notifyModelUpdateStatus()
            }
        }
    }
    
    
    class AccountView: UIView, AccountViewProtocol {
        var presenter: AccountPresentProtocol!
        @IBOutlet private weak var usernameTextField: UITextField!
        
        init() {
            super.init(frame: .zero)
            self.presenter = AccountPresenter(view: self, service: AccountService())
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        private func setupView() {
            //Setup Views - e.g, UITextField
        }
        
        @objc func didTapSaveButton(username: String) {
            presenter.update(username: username)
        }
        
        //MARK: Events from Presenter
        func display(username: String) {
            //Update name
            usernameTextField?.text = username
            print("🟢 Username updated -> \(username)")
        }
        
        func showError(message: String) {
            //Show error message
            print("🔴 \(message)")
        }
    }
    
    let accountView = AccountView()
    accountView.didTapSaveButton(username: "Shawn")
    
    //Prints
    Username updated -> Shawn\n"
    
    //This sample is written by Shawn (Me)

    MVVM: Model-View-ViewModel

    screenshot 2024 03 30 at 3.37.40e280afpm
    viewmodel

    WWDC, Apple gave a sample code using MVVM

    Model–view–viewmodel (MVVM) is an architectural pattern in computer software that facilitates the separation of the development of a graphical user interface (GUI; the view)—be it via a markup language or GUI code—from the development of the business logic or back-end logic (the model) such that the view is not dependent upon any specific model platform.

    The viewmodel of MVVM is a value converter,[1] meaning it is responsible for exposing (converting) the data objects from the model in such a way they can be easily managed and presented. In this respect, the viewmodel is more model than view, and handles most (if not all) of the view’s display logic.[1] The viewmodel may implement a mediator pattern, organizing access to the back-end logic around the set of use cases supported by the view.

    MVVM is a variation of Martin Fowler‘s Presentation Model design pattern.[2][3] It was invented by Microsoft architects Ken Cooper and Ted Peters specifically to simplify event-driven programming of user interfaces. The pattern was incorporated into the Windows Presentation Foundation (WPF) (Microsoft’s .NET graphics system) and Silverlight, WPF’s Internet application derivative.[3] John Gossman, a Microsoft WPF and Silverlight architect, announced MVVM on his blog in 2005.[3][4]

    Model–view–viewmodel is also referred to as model–view–binder, especially in implementations not involving the .NET platform. ZK, a web application framework written in Java, and the JavaScript library KnockoutJS use model–view–binder.

    Model

    Model refers either to a domain model, which represents real state content (an object-oriented approach), or to the data access layer, which represents content (a data-centric approach).[citation needed]

    View

    As in the model–view–controller (MVC) and model–view–presenter (MVP) patterns, the view is the structure, layout, and appearance of what a user sees on the screen.[7] It displays a representation of the model and receives the user’s interaction with the view (mouse clicks, keyboard input, screen tap gestures, etc.), and it forwards the handling of these to the view model via the data binding (properties, event callbacks, etc.) that is defined to link the view and view model.

    View model

    The view model is an abstraction of the view exposing public properties and commands. Instead of the controller of the MVC pattern, or the presenter of the MVP pattern, MVVM has a binder, which automates communication between the view and its bound properties in the view model. The view model has been described as a state of the data in the model.[8]The main difference between the view model and the Presenter in the MVP pattern is that the presenter has a reference to a view, whereas the view model does not. Instead, a view directly binds to properties on the view model to send and receive updates. To function efficiently, this requires a binding technology or generating boilerplate code to do the binding.[7]Under object-oriented programming, the view model can sometimes be referred to as a data transfer object.[9]

    Binder

    Declarative data and command-binding are implicit in the MVVM pattern. In the Microsoft solution stack, the binder is a markup language called XAML.[10] The binder frees the developer from being obliged to write boiler-plate logic to synchronize the view model and view. When implemented outside of the Microsoft stack, the presence of a declarative data binding technology is what makes this pattern possible,[5][11] and without a binder, one would typically use MVP or MVC instead and have to write more boilerplate (or generate it with some other tool).

    Summary

    The MVVM pattern attempts to gain both advantages of separation of functional development provided by MVC, while leveraging the advantages of data bindings and the framework by binding data as close to the pure application model as possible.[3][4][12][clarification needed] It uses the binder, view model, and any business layers’ data-checking features to validate incoming data. The result is that the model and framework drive as much of the operations as possible, eliminating or minimizing application logic which directly manipulates the view (e.g., code-behind).

    Criticism

    John Gossman has criticized the MVVM pattern and its application in specific uses, stating that MVVM can be “overkill” when creating simple user interfaces. For larger applications, he believes that generalizing the viewmodel upfront can be difficult, and that large-scale data binding can lead to lower performance.

    https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel

    MVVM is the most popular architecture of iOS app architecture. Binder is the key different with the others. We can implement this feature using Combine framework or RxSwift.

    screenshot 2024 03 30 at 4.01.16e280afpm

    MVVM-C: Model-View-ViewModel + Coordinator

    screenshot 2024 03 30 at 7.19.24e280afpm

    MVVM-C, There is an extra layer. It called coordinator. It handles UI flows likes present / push.

    VIPER: View Interactor Presenter Entity Router

    screenshot 2024 03 30 at 7.23.01e280afpm

    This architecture is well explained at objc.io blog.

    The main parts of VIPER are:

    • View:
      • displays what it is told to by the Presenter and relays user input back to the Presenter.
      • The View is passive. It waits for the Presenter to give it content to display; it never asks the Presenter for data.
      • The Presenter does not know about the existence of UILabelUIButton, etc. The Presenter only knows about the content it maintains and when it should be displayed. It is up to the View to determine how the content is displayed.
      • The View is an abstract interface, defined in Objective-C with a protocol. A UIViewController or one of its subclasses will implement the View protocol.
      • Views and view controllers also handle user interaction and input. It’s easy to understand why view controllers usually become so large, since they are the easiest place to handle this input to perform some action. To keep our view controllers lean, we need to give them a way to inform interested parties when a user takes certain actions. The view controller shouldn’t be making decisions based on these actions, but it should pass these events along to something that can.
    • Interactor:
      • contains the business logic as specified by a use case.
      • Interactor is a PONSO (Plain Old NSObject) that primarily contains logic, it is easy to develop using TDD.
    • Presenter:
      • contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).
      • The Presenter is a PONSO that mainly consists of logic to drive the UI. It knows when to present the user interface. It gathers input from user interactions so it can update the UI and send requests to an Interactor.
      • Entities are never passed from the Interactor to the Presenter. Instead, simple data structures that have no behavior are passed from the Interactor to the Presenter. This prevents any ‘real work’ from being done in the Presenter. The Presenter can only prepare the data for display in the View.
    • Entity:
      • contains basic model objects used by the Interactor.
      • Entities are only manipulated by the Interactor. The Interactor never passes entities to the presentation layer (i.e. Presenter).
      • Entities also tend to be PONSOs. If you are using Core Data, you will want your managed objects to remain behind your data layer. Interactors should not work with NSManagedObjects.
    • Routing:
      • contains navigation logic for describing which screens are shown in which order.
      • Routes from one screen to another are defined in the wireframes created by an interaction designer.
      • In VIPER, the responsibility for Routing is shared between two objects: the Presenter, and the wireframe. A wireframe object owns the UIWindowUINavigationControllerUIViewController, etc. It is responsible for creating a View/ViewController and installing it in the window.
      • Since the Presenter contains the logic to react to user inputs, it is the Presenter that knows when to navigate to another screen, and which screen to navigate to. Meanwhile, the wireframe knows how to navigate. So, the Presenter will use the wireframe to perform the navigation. Together, they describe a route from one screen to the next.
      • The wireframe is also an obvious place to handle navigation transition animations.

    Application Components Fitting in with VIPER

    • Network
      • Apps are usually much more compelling when they are connected to the network. But where should this networking take place and what should be responsible for initiating it? It’s typically up to the Interactor to initiate a network operation, but it won’t handle the networking code directly. It will ask a dependency, like a network manager or API client. The Interactor may have to aggregate data from multiple sources to provide the information needed to fulfill a use case. Then it’s up to the Presenter to take the data returned by the Interactor and format it for presentation.
    • Data Store
      • A data store is responsible for providing entities to an Interactor. As an Interactor applies its business logic, it will need to retrieve entities from the data store, manipulate the entities, and then put the updated entities back in the data store. The data store manages the persistence of the entities. Entities do not know about the data store, so entities do not know how to persist themselves.
      • The Interactor should not know how to persist the entities either. Sometimes the Interactor may want to use a type of object called a data manager to facilitate its interaction with the data store. The data manager handles more of the store-specific types of operations, like creating fetch requests, building queries, etc. This allows the Interactor to focus more on application logic and not have to know anything about how entities are gathered or persisted. One example of when it makes sense to use a data manager is when you are using Core Data.
    • TDD to develop an Interactor
      • it is possible to switch out the production data store with a test double/mock. Not talking to a remote server (for a web service) or touching the disk (for a database) allows your tests to be faster and more repeatable.
      • One reason to keep the data store as a distinct layer with clear boundaries is that it allows you to delay choosing a specific persistence technology. If your data store is a single class, you can start your app with a basic persistence strategy, and then upgrade to SQLite or Core Data later if and when it makes sense to do so, all without changing anything else in your application’s code base.
      • Using Core Data in an iOS project can often spark more debate than architecture itself. However, using Core Data with VIPER can be the best Core Data experience you’ve ever had. Core Data is a great tool for persisting data while maintaining fast access and a low-memory footprint. But it has a habit of snaking its NSManagedObjectContext tendrils all throughout an app’s implementation files, particularly where they shouldn’t be. VIPER keeps Core Data where it should be: at the data store layer.
      • In the to-do list example, the only two parts of the app that know that Core Data is being used are the data store itself, which sets up the Core Data stack, and the data manager. The data manager performs a fetch request, converts the NSManagedObjects returned by the data store into standard PONSO model objects, and passes those back to the business logic layer. That way, the core of the application is never dependent on Core Data, and as a bonus, you never have to worry about stale or poorly threaded NSManagedObjects gunking up the works.
    • Storyboard
      • we tend to make is to choose not to use segues. There may be some cases where using the segue makes sense, but the danger with segues is they make it very difficult to keep the separation between screens — as well as between UI and application logic — intact. As a rule of thumb, we try not to use segues if implementing the prepareForSegue method appears necessary.
      • Otherwise, storyboards are a great way to implement the layout for your user interface, especially while using Auto Layout. We chose to implement both screens for the to-do list example using a storyboard, and use code such as this to perform our own navigation:
    • Writing a test cases
      • Building the Interactor first is a natural fit with TDD. If you develop the Interactor first, followed by the Presenter, you get to build out a suite of tests around those layers first and lay the foundation for implementing those use cases. You can iterate quickly on those classes, because you won’t have to interact with the UI in order to test them. Then, when you go to develop the View, you’ll have a working and tested logic and presentation layer to connect to it. By the time you finish developing the View, you might find that the first time you run the app everything just works, because all your passing tests tell you it will work.

    Conclusion

    At its core, VIPER is an architecture based on the Single Responsibility Principle.

    https://www.objc.io/issues/13-architecture/viper/

    You can check an swift version example

    App Architecture in SwiftUI

    SwiftUI provide a powerful features like StateObject, ObservedObject, ObservableObject, State, Bindable, Binding and more. If you interested in, check this article

    Managing user interface state

    screenshot 2024 03 30 at 6.49.12e280afpm

    Encapsulate view-specific data within your app’s view hierarchy to make your views reusable

    Store data as state in the least common ancestor of the views that need the data to establish a single source of truth that’s shared across views. Provide the data as read-only through a Swift property, or create a two-way connection to the state with a binding. SwiftUI watches for changes in the data, and updates any affected views as needed.

    Don’t use state properties for persistent storage because the life cycle of state variables mirrors the view life cycle. Instead, use them to manage transient state that only affects the user interface, like the highlight state of a button, filter settings, or the currently selected list item. You might also find this kind of storage convenient while you prototype, before you’re ready to make changes to your app’s data model.

    https://developer.apple.com/documentation/swiftui/managing-user-interface-state

    Managing model data

    💡 Below image is no longer available. It has disappeared after Apple introduced Observable macro.

    screenshot 2024 03 30 at 6.59.47e280afpm

    Model data

    Manage the data that your app uses to drive its interface.

    SwiftUI offers a declarative approach to user interface design. As you compose a hierarchy of views, you also indicate data dependencies for the views. When the data changes, either due to an external event or because of an action that the user performs, SwiftUI automatically updates the affected parts of the interface. As a result, the framework automatically performs most of the work that view controllers traditionally do.

    The framework provides tools, like state variables and bindings, for connecting your app’s data to the user interface. These tools help you maintain a single source of truth for every piece of data in your app, in part by reducing the amount of glue logic you write. Select the tool that best suits the task you need to perform:

    • Manage transient UI state locally within a view by wrapping value types as State properties.
    • Share a reference to a source of truth, like local state, using the Binding property wrapper.
    • Connect to and observe reference model data in views by applying the Observable() macro to the model data type. Instantiate an observable model data type directly in a view using a State property. Share the observable model data with other views in the hierarchy without passing a reference using the Environment property wrapper.

    https://developer.apple.com/documentation/swiftui/model-data