Showing posts with label Coding Challanges. Show all posts
Showing posts with label Coding Challanges. Show all posts

Code Challenge: Number of Islands in Kotlin

The Number of Islands problem is a common interview question that involves counting the number of islands in a 2D grid. Each island is made up of connected pieces of land (denoted as '1') surrounded by water (denoted as '0'). The challenge is to count how many separate islands exist in the grid, where an island is formed by horizontally or vertically adjacent lands.



We will discuss multiple ways to solve this problem, explaining their pros and cons. Let's dive into solving this problem using Kotlin.


Problem Definition

Given a 2D binary grid grid, return the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. The grid is surrounded by water on all sides.

Example 1:

Input:

[
  ["1", "1", "1", "1", "0"],
  ["1", "1", "0", "1", "0"],
  ["1", "1", "0", "0", "0"],
  ["0", "0", "0", "0", "0"]
]

Output:

1

Example 2:

Input:

[
  ["1", "1", "0", "0", "0"],
  ["1", "1", "0", "0", "0"],
  ["0", "0", "1", "0", "0"],
  ["0", "0", "0", "1", "1"]
]

Output:

3

Approach 1: Depth-First Search (DFS)

The most intuitive approach is to use Depth-First Search (DFS). We start from each land cell ('1'), mark it as visited (or change it to water '0'), and recursively check its adjacent cells (up, down, left, right). Every time we find an unvisited land cell, we count it as a new island.

Algorithm:

  1. Traverse the grid.
  2. If we find a '1', increment the island count and use DFS to mark the entire island as visited.
  3. For each DFS, recursively mark the neighboring land cells.

Kotlin Implementation:

fun numIslands(grid: Array<CharArray>): Int {
    if (grid.isEmpty()) return 0
    var count = 0

    // Define DFS function
    fun dfs(grid: Array<CharArray>, i: Int, j: Int) {
        // Return if out of bounds or at water
        if (i < 0 || i >= grid.size || j < 0 || j >= grid[0].size || grid[i][j] == '0') return
        // Mark the land as visited
        grid[i][j] = '0'
        // Visit all 4 adjacent cells
        dfs(grid, i + 1, j) // down
        dfs(grid, i - 1, j) // up
        dfs(grid, i, j + 1) // right
        dfs(grid, i, j - 1) // left
    }

    // Iterate over the grid
    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                // Found a new island
                count++
                dfs(grid, i, j)
            }
        }
    }
    return count
}

Time Complexity:

  • O(m * n), where m is the number of rows and n is the number of columns. Each cell is visited once.

Space Complexity:

  • O(m * n) in the worst case (if the entire grid is land), as we may need to store all cells in the call stack due to recursion.

Approach 2: Breadth-First Search (BFS)

We can also use Breadth-First Search (BFS). Instead of using recursion like in DFS, we use a queue to explore all adjacent cells iteratively. The process is similar, but the main difference lies in the order of exploration.

Algorithm:

  1. Start from an unvisited land cell ('1').
  2. Use a queue to explore all adjacent land cells and mark them as visited.
  3. Each BFS initiation represents a new island.

Kotlin Implementation:

fun numIslands(grid: Array<CharArray>): Int {
    if (grid.isEmpty()) return 0
    var count = 0
    val directions = arrayOf(intArrayOf(0, 1), intArrayOf(1, 0), intArrayOf(0, -1), intArrayOf(-1, 0))

    fun bfs(i: Int, j: Int) {
        val queue: LinkedList<Pair<Int, Int>>= LinkedList()
        queue.offer(Pair(i, j))
        grid[i][j] = '0' // Mark the starting cell as visited

        while (queue.isNotEmpty()) {
            val (x, y) = queue.poll()
            for (dir in directions) {
                val newX = x + dir[0]
                val newY = y + dir[1]
                if (newX in grid.indices && newY in grid[0].indices && grid[newX][newY] == '1') {
                    grid[newX][newY] = '0' // Mark as visited
                    queue.offer(Pair(newX, newY))
                }
            }
        }
    }

    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                count++
                bfs(i, j)
            }
        }
    }
    return count
}

Time Complexity:

  • O(m * n), where m is the number of rows and n is the number of columns. Each cell is visited once.

Space Complexity:

  • O(m * n), which is required for the queue in the worst case.

Approach 3: Union-Find (Disjoint Set)

The Union-Find (or Disjoint Set) approach is another efficient way to solve this problem. The idea is to treat each land cell as an individual component and then union adjacent land cells. Once all unions are complete, the number of islands is simply the number of disjoint sets.

Algorithm:

  1. Initialize each land cell as a separate island.
  2. For each neighboring land cell, perform a union operation.
  3. The number of islands will be the number of disjoint sets.

Kotlin Implementation:

class UnionFind(private val m: Int, private val n: Int) {
    private val parent = IntArray(m * n) { it }

    fun find(x: Int): Int {
        if (parent[x] != x) parent[x] = find(parent[x]) // Path compression
        return parent[x]
    }

    fun union(x: Int, y: Int) {
        val rootX = find(x)
        val rootY = find(y)
        if (rootX != rootY) parent[rootX] = rootY
    }

    fun getCount(): Int {
        return parent.count { it == it }
    }
}

fun numIslands(grid: Array<CharArray>): Int {
    if (grid.isEmpty()) return 0
    val m = grid.size
    val n = grid[0].size
    val uf = UnionFind(m, n)

    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                val index = i * n + j
                // Try to union with adjacent cells
                if (i + 1 &lt; m &amp;&amp; grid[i + 1][j] == '1') uf.union(index, (i + 1) * n + j)
                if (j + 1 &lt; n &amp;&amp; grid[i][j + 1] == '1') uf.union(index, i * n + (j + 1))
            }
        }
    }
    val islands = mutableSetOf&lt;Int&gt;()
    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                islands.add(uf.find(i * n + j))
            }
        }
    }
    return islands.size
}

Time Complexity:

  • O(m * n), as we perform a union operation for each adjacent land cell.

Space Complexity:

  • O(m * n) for the union-find parent array.

Calling in main():

fun main() {
    val grid1 = arrayOf(
        charArrayOf('1', '1', '1', '1', '0'),
        charArrayOf('1', '1', '0', '1', '0'),
        charArrayOf('1', '1', '0', '0', '0'),
        charArrayOf('0', '0', '0', '0', '0')
    )
    println("Number of Islands : ${numIslands(grid1)}")  // Output: 1
    
    val grid2 = arrayOf(
        charArrayOf('1', '1', '0', '0', '0'),
        charArrayOf('1', '1', '0', '0', '0'),
        charArrayOf('0', '0', '1', '0', '0'),
        charArrayOf('0', '0', '0', '1', '1')
    )
    println("Number of Islands : ${numIslands(grid2)}")  // Output: 3
}



Which Solution is Best?

  1. DFS/BFS (Approaches 1 & 2): These are the simplest and most intuitive solutions. Both have a time complexity of O(m * n), which is optimal for this problem. DFS uses recursion, which might run into issues with large grids due to stack overflow, but BFS avoids this problem by using an iterative approach. If you want simplicity and reliability, BFS is preferred.

  2. Union-Find (Approach 3): This approach is more advanced and has a similar time complexity of O(m * n). However, it can be more difficult to understand and implement. It also performs well with path compression and union by rank, but for this problem, the DFS/BFS approach is usually sufficient and easier to implement.

Conclusion

For this problem, BFS is the recommended solution due to its iterative nature, which avoids recursion issues with large grids, while still being efficient and easy to understand.


Full Problem description in LeetCode


Thank you for reading my latest article! I would greatly appreciate your feedback to improve my future posts. 💬 Was the information clear and valuable? Are there any areas you think could be improved? Please share your thoughts in the comments or reach out directly. Your insights are highly valued. 👇😊.  Happy coding! 💻✨

Code Challenge: Designing a Parking Lot System in Kotlin

Designing a parking lot system is an excellent exercise for learning object-oriented programming (OOP) principles, modularity, and scalability. This article explains a step-by-step implementation of a parking lot system in Kotlin, focusing on clarity and logical structure. The content is written to cater to developers at all levels.


Problem Definition

We aim to build a parking lot system with the following requirements:

  1. Vehicle Types: Motorcycles, Cars, and Vans.
  2. Spot Types: Motorcycle spots, compact spots, and large spots.
  3. Parking Rules:
    • Motorcycles can park in any spot.
    • Cars can park in compact or larger spots.
    • Vans need three consecutive large spots.
  4. System Features:
    • Track available spots and their types.
    • Determine if the parking lot is full or empty.
    • Check if specific types of spots (e.g., motorcycle spots) are full.

High-Level Design

The solution follows a modular approach:

  1. Enums: Define vehicle and parking spot types.
  2. Interfaces: Abstract shared functionality for vehicles and spots.
  3. Classes: Concrete implementations for vehicles, spots, and parking lot management.
  4. Controller: High-level interface for interacting with the system.

Each section below breaks down the design in detail.


1. Defining Enums

Enums are ideal for defining fixed categories, like vehicle and spot types.

enum class VehicleType { MOTORCYCLE, CAR, VAN }
enum class SpotType { MOTORCYCLE, COMPACT, LARGE }
  • What This Does:

    • VehicleType categorizes vehicles (Motorcycle, Car, Van).
    • SpotType categorizes parking spots (Motorcycle, Compact, Large).
  • Why It Matters:

    • Enums make code more readable and maintainable. For example, instead of using arbitrary strings, you can use VehicleType.CAR.

2. Abstracting Vehicles

We use an interface to define the behavior of all vehicles. Specific vehicle types inherit this interface and add their unique properties.

interface Vehicle {
    val type: VehicleType
    val requiredSpots: Int
}

class Motorcycle : Vehicle {
    override val type = VehicleType.MOTORCYCLE
    override val requiredSpots = 1
}

class Car : Vehicle {
    override val type = VehicleType.CAR
    override val requiredSpots = 1
}

class Van : Vehicle {
    override val type = VehicleType.VAN
    override val requiredSpots = 3
}
  • What This Does:

    • Vehicle defines shared properties: type and requiredSpots.
    • Motorcycle, Car, and Van implement specific logic, like how many spots they need.
  • Why It Matters:

    • Abstraction allows flexibility. If a new vehicle type is added, you only need to create a new class without changing the existing code.

3. Abstracting Parking Spots

Parking spots are represented by an interface and a concrete class.

interface ParkingSpot {
    val id: Int
    val type: SpotType
    var isOccupied: Boolean
}

class GenericParkingSpot(
    override val id: Int,
    override val type: SpotType
) : ParkingSpot {
    override var isOccupied = false
}
  • What This Does:

    • ParkingSpot defines properties like id, type, and isOccupied.
    • GenericParkingSpot implements these properties.
  • Why It Matters:

    • Decoupling spot behavior from its implementation makes the code flexible. For example, adding electric vehicle spots in the future requires only creating a new class.

4. Managing the Parking Lot

The ParkingLotManager class handles the core functionality:

  1. Initializing parking spots.
  2. Allocating spots for vehicles.
  3. Removing vehicles.
  4. Providing status updates.
class ParkingLotManager(
    motorcycleSpots: Int,
    compactSpots: Int,
    largeSpots: Int
) {
    private val spots: MutableList<ParkingSpot> = mutableListOf()

    init {
        repeat(motorcycleSpots) { spots.add(GenericParkingSpot(spots.size + 1, SpotType.MOTORCYCLE)) }
        repeat(compactSpots) { spots.add(GenericParkingSpot(spots.size + 1, SpotType.COMPACT)) }
        repeat(largeSpots) { spots.add(GenericParkingSpot(spots.size + 1, SpotType.LARGE)) }
    }

    fun parkVehicle(vehicle: Vehicle): Boolean {
        val availableSpots = spots.filter { !it.isOccupied && it.type.ordinal >= vehicle.type.ordinal }

        if (availableSpots.size >= vehicle.requiredSpots) {
            availableSpots.take(vehicle.requiredSpots).forEach { it.isOccupied = true }
            println("${vehicle.type} parked successfully.")
            return true
        }
        println("No space available for ${vehicle.type}.")
        return false
    }

    fun removeVehicle(vehicle: Vehicle) {
        val occupiedSpots = spots.filter { it.isOccupied && it.type.ordinal >= vehicle.type.ordinal }
        if (occupiedSpots.size >= vehicle.requiredSpots) {
            occupiedSpots.take(vehicle.requiredSpots).forEach { it.isOccupied = false }
            println("${vehicle.type} removed successfully.")
        } else {
            println("No vehicle of type ${vehicle.type} found to remove.")
        }
    }

    fun getRemainingSpots(): Int = spots.count { !it.isOccupied }
    fun isFull(): Boolean = spots.none { !it.isOccupied }
    fun isEmpty(): Boolean = spots.all { !it.isOccupied }
}
  • What This Does:

    • Initializes spots based on configuration.
    • Handles the logic for parking and removing vehicles.
    • Tracks the parking lot's status.
  • Why It Matters:

    • Centralized management makes it easier to add new features, like reserved spots or dynamic pricing.

5. Simplifying User Interaction

The ParkingLotController abstracts parking lot management for the user. It combines common operations like parking, removing, and querying into a single interface.

class ParkingLotController(private val parkingLotManager: ParkingLotManager) {
    fun park(vehicle: Vehicle) {
        parkingLotManager.parkVehicle(vehicle)
    }

    fun remove(vehicle: Vehicle) {
        parkingLotManager.removeVehicle(vehicle)
    }

    fun displayStatus() {
        println("Remaining Spots: ${parkingLotManager.getRemainingSpots()}")
        println("Is Full: ${parkingLotManager.isFull()}")
        println("Is Empty: ${parkingLotManager.isEmpty()}")
    }
}
  • What This Does:

    • Simplifies interaction with the parking lot system.
    • Focuses on common actions like parking and querying status.
  • Why It Matters:

    • Abstracting complexity improves usability for developers using the system.

6. Putting It All Together

The main function demonstrates how all components work together.

fun main() {
    val parkingLotManager = ParkingLotManager(motorcycleSpots = 5, compactSpots = 10, largeSpots = 3)
    val controller = ParkingLotController(parkingLotManager)

    val motorcycle = Motorcycle()
    val car = Car()
    val van = Van()

    controller.park(motorcycle)
    controller.park(car)
    controller.park(van)
    controller.displayStatus()

    controller.remove(car)
    controller.displayStatus()
}
  • What This Does:
    • Creates a parking lot with specified spots.
    • Parks and removes vehicles.
    • Displays the parking lot's status after each operation.

Advantages of This Design

  1. Modular and Maintainable:

    • Each class/interface has a single responsibility.
    • The code is easier to understand and maintain.
  2. Scalable:

    • Adding new vehicle or spot types is simple (e.g., adding EV spots or trucks).
  3. Reusable:

    • Interfaces (Vehicle, ParkingSpot) ensure reusability and extensibility.
  4. Adheres to OOP Principles:

    • Encapsulation: Hides the implementation details of parking logic.
    • Polymorphism: Handles different vehicle types using a common interface.
    • Abstraction: Separates high-level logic from lower-level details.

Summary

This solution demonstrates a modular, extensible, and maintainable approach to designing a parking lot system in Kotlin. Key highlights include:

  • Enums for categorization.
  • Interfaces for abstraction.
  • Classes for specific implementations.
  • Centralized Management for parking logic.
  • Simplified Interaction through a controller.

This design adheres to core OOP principles, such as encapsulation, abstraction, and polymorphism. It ensures that adding new features, such as electric vehicle spots or dynamic pricing, is straightforward.

Whether you’re a beginner learning Kotlin or an experienced developer designing complex systems, this approach provides a solid foundation for building scalable applications.

More details of problem go to LeetCode 

HappyCoding 

#Kotlin #Android #CodeChallenge

Coding Challenge: Interview - > Shorten Url

URL shortening involves generating abbreviated versions of long URLs, referred to as "short links." When users access these short links, they are automatically redirected to the original, longer URL. Short links offer various benefits, such as saving space in display, print, messages, or tweets. Furthermore, shorter URLs decrease the likelihood of users making typing errors.

import java.util.*
 
class URLShortener {
    private val idToUrlMap = HashMap<String, String>()
    private val urlToIdMap = HashMap<String, String>()
    private val BASE_URL = "http://short.url/"
    private val ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 
    fun shortenURL(longURL: String): String {
        if (urlToIdMap.containsKey(longURL)) {
            return BASE_URL + urlToIdMap[longURL]
        }
        val id = generateID()
        idToUrlMap[id] = longURL
        urlToIdMap[longURL] = id
        return BASE_URL + id
    }
 
    fun expandURL(shortURL: String): String {
        val id = shortURL.substring(BASE_URL.length)
        return idToUrlMap[id] ?: "URL not found"
    }
 
    private fun generateID(): String {
        val random = Random()
        val sb = StringBuilder()
        repeat(6) {
            sb.append(ALPHABET[random.nextInt(ALPHABET.length)])
        }
        return sb.toString()
    }
}
 
fun main() {
    val urlShortener = URLShortener()
    val longURL = "https://www.example.com"
    val shortURL = urlShortener.shortenURL(longURL)
    println("Shortened URL: $shortURL")
    val expandedURL = urlShortener.expandURL(shortURL)
    println("Expanded URL: $expandedURL")
}
 

Details of above code with explanation

  • idToUrlMap: This HashMap maps shortened IDs (generated by the shortening process) to their corresponding original URLs. It facilitates the expansion process.
  • urlToIdMap: This HashMap maps original URLs to their corresponding shortened IDs. It helps avoid duplication of shortened URLs for the same original URL.
  • BASE_URL: This constant string represents the base URL for the shortened URLs. All shortened URLs generated by this URL shortener will start with this base URL.
  • ALPHABET: This string contains the characters used to generate the random alphanumeric IDs for shortening URLs.
  • shortenURL(longURL: String): This method takes a long URL as input and generates a shortened URL for it. If the long URL has already been shortened before, it retrieves the existing shortened URL. Otherwise, it generates a new shortened ID, maps it to the long URL, and returns the shortened URL.
  • expandURL(shortURL: String): This method takes a shortened URL as input and returns the corresponding original URL. It extracts the ID from the shortened URL and looks it up in the idToUrlMap. If the ID exists in the map, it returns the corresponding original URL; otherwise, it indicates that the URL is not found.
  • generateID(): This private method generates a random alphanumeric ID of length 6 using characters from the ALPHABET string. It ensures uniqueness for each shortened URL.
Happy Coding ✌

Coding Challenge: Interview - > Rotate Image

 You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).

You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.

 Example 1:

Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [[7,4,1],[8,5,2],[9,6,3]]

Example 2:

Input: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

 

Constraints:

  • n == matrix.length == matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

Solutions:

fun rotate(matrix: Array<IntArray>) {
    val n = matrix.size
    
    // Transpose the matrix
    for (i in 0 until n) {
        for (j in i until n) {
            val temp = matrix[i][j]
            matrix[i][j] = matrix[j][i]
            matrix[j][i] = temp
        }
    }
    
    // Reverse each row
    for (i in 0 until n) {
        var left = 0
        var right = n - 1
        while (left < right) {
            val temp = matrix[i][left]
            matrix[i][left] = matrix[i][right]
            matrix[i][right] = temp
            left++
            right--
        }
    }
}


Here is the more details of each details

Certainly! Let's break down the solution step by step:

  1. Transpose the matrix:

    • To transpose the matrix means to swap its rows and columns. We iterate over the upper triangle of the matrix (i.e., i from 0 to n-1, j from i to n-1) and swap each element with its corresponding element across the diagonal.
    • For example, if we have a matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]], transposing it will give us [[1, 4, 7], [2, 5, 8], [3, 6, 9]].
  2. Reverse each row:

    • After transposing the matrix, we iterate over each row, and for each row, we reverse its elements.
    • This reversal effectively rotates each row by 180 degrees.
    • For example, if we have a transposed matrix [[1, 4, 7], [2, 5, 8], [3, 6, 9]], reversing each row will give us [[7, 4, 1], [8, 5, 2], [9, 6, 3]].

By performing these two operations, we achieve the desired result of rotating the matrix by 90 degrees clockwise. The key insight is that transposing the matrix swaps rows and columns, effectively rotating it by 90 degrees counterclockwise. Then, reversing each row completes the rotation to 90 degrees clockwise. This solution modifies the input matrix in-place without using any extra space.


Happy Coding ✌!