Swift Coding Test Course, Binary Search

1. Overview of Binary Search

Binary Search is an algorithm used to find the position of a specific value in a sorted array.
It is very efficient as it divides the array in half to find the desired value,
having a time complexity of O(log n) in both average and worst cases.
This is significantly better than the O(n) of linear search.

1.1 Principle of Binary Search

Binary Search proceeds with the following steps:

  1. Check if the array to be searched is sorted.
  2. Set the start index and end index.
  3. Calculate the middle index.
  4. Compare the middle value to the value you want to find.
  5. If the value to be found is less than the middle value, set the end index to middle index – 1,
    and if it’s greater, set the start index to middle index + 1.
  6. Repeat until the value is found or the start index is greater than the end index.

2. Algorithm Problems

Now, let’s look at a problem that utilizes binary search.

Problem: Finding the Index of a Specific Number in an Array

Given an integer array nums and an integer target, 
write a function that returns the index of target if it exists in the array nums, 
or -1 if it does not exist.

Example:
Input: nums = [-1,0,3,5,9,12], target = 9
Output: 4

Input: nums = [-1,0,3,5,9,12], target = 2
Output: -1

3. Problem Solving Process

To solve the problem, we will use the binary search algorithm to find the target value in the array.
I will explain step by step.

3.1 Function Definition

First, we define the binarySearch function that will perform the binary search.
This function takes the array nums and the target value as arguments.


func binarySearch(nums: [Int], target: Int) -> Int {
    var left = 0
    var right = nums.count - 1

    while left <= right {
        let mid = left + (right - left) / 2

        if nums[mid] == target {
            return mid
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1
}

3.2 Variable Initialization

Initialize the variables left and right to 0 and the length of the array - 1, respectively.
left represents the starting index of the search range, and right represents the ending index.

3.3 Calculating the Middle Value

Use the while loop to repeat until left is less than or equal to right.
In each iteration, calculate the middle index mid.
When calculating the middle value, use left + (right - left) / 2 to prevent overflow.

3.4 Comparing the Target

If the middle value nums[mid] equals target, return that index mid.
If nums[mid] is less than target,
set left to mid + 1 to search the right half.
Conversely, if nums[mid] is greater than target,
set right to mid - 1 to search the left half.

3.5 Returning the Result

When the loop ends, it means target does not exist in the array, so return -1.

4. Full Code


func binarySearch(nums: [Int], target: Int) -> Int {
    var left = 0
    var right = nums.count - 1

    while left <= right {
        let mid = left + (right - left) / 2

        if nums[mid] == target {
            return mid
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1
}

// Example usage
let nums = [-1, 0, 3, 5, 9, 12]
let target = 9
let result = binarySearch(nums: nums, target: target)
print(result) // 4

5. Advantages and Disadvantages of Binary Search

5.1 Advantages

The main advantage of binary search is its fast search speed.
It shows significantly higher performance compared to linear search when dealing with very large datasets.

5.2 Disadvantages

However, a disadvantage of using binary search is that the data must be sorted.
Frequent insertion and deletion of data may require separate sorting operations.

6. Conclusion

Binary search is an efficient searching method and is one of the topics frequently asked in coding tests.
Through the above problem, I hope you have understood the principles and implementation methods of binary search,
and gained useful experience in writing code in Swift.

7. Additional Practice Problems

To deepen your understanding of binary search, try solving the additional problems below.

  • Write a function that finds and returns all indices of a specific number in a given integer array.
  • Implement a function that finds the first and last positions in a sorted array.
  • Write a function that determines whether there is a combination of two numbers in an integer array that adds up to a specific number.

Swift Coding Test Course, Binary Graph Discrimination

What is a bipartite graph?
A bipartite graph is a graph whose vertex set can be divided into two mutually exclusive subsets. In other words, it is a division such that all edges of the graph only exist between vertices of the two different sets.
The most common example of a bipartite graph is the “matching” problem. For instance, when matching students to classes, students and classes can be entered as each of the respective sets.
Bipartite graphs have the property that they can always be colored with two colors.

Problem Description

Write a function to determine if the given undirected graph is bipartite.
The given graph is presented in the form of an adjacency list, and the vertices are connected from 0 to n-1.
The function should return true if the graph is bipartite, and false otherwise.

Input Example

    n = 4
    edges = [[0, 1], [0, 3], [1, 2], [2, 3]]
    

Output Example

    false
    

Problem Solving Process

  1. Understanding the Structure of the Graph

    The given graph consists of nodes and edges, with each node connected to other nodes.
    We will represent the graph in an undirected linked list format.
    Many programming languages, including Java and Swift, can implement this structure using arrays or hashmaps.

  2. Properties of Bipartite Graphs and Search Methods

    A bipartite graph can be divided into two sets of vertices,
    where all adjacent vertices must belong to different sets. Utilizing this property, we can employ depth-first search (DFS) or breadth-first search (BFS)
    as an approach to color the graph.

  3. Exploring the Graph Using DFS or BFS

    We start exploring the graph by coloring each vertex.
    Two colors are used (e.g., 1 and -1), and if we revisit a node that is already colored,
    we can determine that it is not a bipartite graph if the colors match.

Code Implementation

We will now implement an algorithm to determine bipartiteness in Swift.


    class Solution {
        func isBipartite(_ graph: [[Int]]) -> Bool {
            let n = graph.count
            var color = Array(repeating: -1, count: n) // -1 means uncolored
            
            for i in 0..

Examples and Explanation

The above code traverses the given graph, coloring the nodes and checking for re-visits to determine bipartiteness.
In the example above, the graph takes the following form:

    0 -- 1
    |    |
    3 -- 2
    

In this case, nodes 0 and 1 have different colors, 1 and 2 have different colors, and 2 and 3 have different colors.
However, nodes 0 and 3 have the same color, indicating that it is not a bipartite graph.
This can be verified through BFS exploration.

Conclusion

In this article, we explained the concept of bipartite graphs and the process of their determination,
and we explored how to implement this in Swift.
This problem is useful for laying the foundations of algorithms and data structures,
and it can be applied in various interview questions.
Therefore, understanding bipartite graphs and coloring algorithms deeply through this problem is crucial.

References

Swift Coding Test Course, Euclidean Algorithm

1. What is the Euclidean Algorithm?

The Euclidean Algorithm is an efficient method for calculating the greatest common divisor (GCD) of two integers.
This method was first presented as an algorithm by the ancient Greek mathematician Euclid in his book Elements.
The greatest common divisor of two integers a and b has the following properties:

  • gcd(a, 0) = a
  • gcd(a, b) = gcd(b, a mod b)

Using these two properties, the greatest common divisor can be calculated repeatedly. The time complexity of the Euclidean Algorithm is
O(log(min(a, b))), which is very efficient.

2. Example of the Euclidean Algorithm

Let’s find the greatest common divisor of the two integers 48 and 18.

        gcd(48, 18)
        1. 48 mod 18 = 12
        2. gcd(18, 12)
        3. 18 mod 12 = 6
        4. gcd(12, 6)
        5. 12 mod 6 = 0
        6. gcd(6, 0) = 6
    

Therefore, the greatest common divisor of 48 and 18 is 6.

3. Problem Using the Euclidean Algorithm

Problem: Find the GCD of Two Numbers

For the given two integers a and b, write a function to find their greatest common divisor.
Let’s implement this using Swift.

Problem Constraints

  • 0 < a, b < 231
  • The return value of the function is the greatest common divisor of the two numbers.

4. Problem Solving Process

To solve the given problem, we will implement it in Swift.
First, we will define the structure of the function to solve this problem.

        func gcd(a: Int, b: Int) -> Int {
            // Calculate the greatest common divisor when both a and b are 0
            if b == 0 {
                return a
            } else {
                // Calculate gcd recursively
                return gcd(b, a % b)
            }
        }
    

Using the above function, we can find the greatest common divisor of the two numbers a and b. Here, a is one of the two numbers,
b is the other number, and this function is called recursively until b becomes 0.
At the point when b becomes 0, a is returned as the greatest common divisor.

4.1. Example Code

Below is the complete code for finding the greatest common divisor using the Euclidean Algorithm.

        func gcd(a: Int, b: Int) -> Int {
            if b == 0 {
                return a
            } else {
                return gcd(b, a % b)
            }
        }

        // Example execution
        let a = 48
        let b = 18
        let result = gcd(a: a, b: b)
        print("Greatest Common Divisor: \(result)") // Result: Greatest Common Divisor: 6
    

5. Implementing the Euclidean Algorithm in Swift

The following is an example of implementing the Euclidean Algorithm using a loop in Swift.
Sometimes using a loop can be more efficient in terms of memory usage compared to recursive calls.

        func gcdIterative(a: Int, b: Int) -> Int {
            var a = a
            var b = b
            while b != 0 {
                let temp = b
                b = a % b
                a = temp
            }
            return a
        }

        // Example execution
        let resultIterative = gcdIterative(a: a, b: b)
        print("Greatest Common Divisor through Loop: \(resultIterative)") // Result: Greatest Common Divisor through Loop: 6
    

5.1. Practice with Various Cases

You can practice by applying these functions to various pairs of integers.
For example, try finding the greatest common divisor of 56 and 98, 101 and 103, and so on.

        print("gcd(56, 98) = \(gcd(56, 98))") // Result: 14
        print("gcd(101, 103) = \(gcd(101, 103))") // Result: 1
    

6. Conclusion

The Euclidean Algorithm is a simple but very effective algorithm for finding the greatest common divisor.
We have looked at how to implement it in Swift. We were able to learn both methods, iterative and recursive,
and it’s good to consider which method is more efficient depending on the situation.

Such algorithms are often featured in various programming competitions and coding tests, so
it is important to practice thoroughly to become familiar with them.
In addition to the Euclidean Algorithm, I hope you study various algorithms and problem-solving methods to
further enhance your coding skills. Thank you!

Swift Coding Test Course, Union Find

Introduction

Currently, understanding algorithms and data structures is essential in the IT industry. Especially for developers preparing for employment, it is crucial to have problem-solving skills based on this knowledge. In this course, we will take a closer look at the ‘Union-Find’ algorithm and explain the problem-solving process using it step by step.

What is Union-Find?

Union-Find is a data structure primarily used to handle operations on sets, supporting two main operations:

  • Union: The operation of merging two sets
  • Find: The operation of finding which set a particular element belongs to

This data structure is useful for solving the Disjoint Set problem. Union-Find increases efficiency using optimization techniques called ‘Path Compression’ and ‘Rank’.

Problem Definition

To define the problem, let’s set up a hypothetical situation.

Problem: Given N elements, write a program to determine whether multiple pairs of elements belong to the same set.

The number of elements N and the number of pairs M are provided. For each pair, if they belong to the ‘same set’, print “YES”, and if they belong to ‘different sets’, print “NO”.

Example Problem

Input:

5 4
1 2
1 3
2 3
4 5
1 4
            

Output:

NO
YES
            

In the above example, the first pair (1, 4) belongs to different sets, so it outputs “NO”, and the second pair (1, 4) belongs to the same set, so it outputs “YES”.

Implementing the Union-Find Algorithm

Now, let’s implement the Union-Find algorithm in Swift. We will write the code reflecting the basic concepts.

class UnionFind {
    private var parent: [Int]
    private var rank: [Int]
    
    init(size: Int) {
        self.parent = Array(0.. Int {
        if p != parent[p] {
            parent[p] = find(parent[p]) // Path Compression
        }
        return parent[p]
    }
    
    func union(_ p: Int, _ q: Int) {
        let rootP = find(p)
        let rootQ = find(q)
        
        if rootP != rootQ {
            if rank[rootP] > rank[rootQ] {
                parent[rootQ] = rootP
            } else if rank[rootP] < rank[rootQ] {
                parent[rootP] = rootQ
            } else {
                parent[rootQ] = rootP
                rank[rootP] += 1
            }
        }
    }
}
            

Problem Solving Process

To solve the problem, we will follow these steps.

  1. Parse the input to obtain the number of elements N and the number of pairs M.
  2. Use the UnionFind class to manage the sets of elements.
  3. Perform the union operation for each pair to merge the sets.
  4. Perform the find operation for each pair and print the results.

The code is as follows.

import Foundation

let input = readLine()!.split(separator: " ").map { Int($0)! }
let n = input[0]
let m = input[1]

let unionFind = UnionFind(size: n + 1)

for _ in 0..

Conclusion

In this course, we have implemented the theoretical background of the Union-Find algorithm and the problem-solving process in Swift. Union-Find is one of the very useful data structures for graph-related problems. By utilizing this data structure, complex problems can be effectively solved.

I hope you continue to explore various algorithms to aid in your coding test preparation, and I look forward to introducing other algorithms in the next course.

Swift Coding Test Course, Topological Sorting

Coding tests are important exams for many software developers. In particular, the ability to solve algorithmic problems is a crucial factor for interviewers to evaluate applicants’ problem-solving skills. This course covers the essence and application of an algorithm called ‘Topological Sorting.’ Topological sorting is a concept mostly used in graph theory, which is a way to linearly order the vertices of a directed graph.

1. Concept of Topological Sorting

Topological sorting is the process of listing each vertex in a Directed Acyclic Graph (DAG) in order. The condition that one vertex can come before another is determined by the direction of the edges. That is, if there is an edge from vertex A to vertex B, then in topological sorting, vertex A must come before vertex B.

2. Problem Description

Problem: Write a program to print the order of prerequisite subjects required to complete all courses based on the given relationship between classes and their prerequisites.

Input Format

– The first line contains two integers N (1 ≤ N ≤ 10^6) and M (1 ≤ M ≤ 10^6), where N is the number of courses and M is the number of prerequisite relationships between the courses.

– The next M lines each contain two integers A and B, indicating that A is a prerequisite for B.

Output Format

– Print the sorted order of courses that satisfy the prerequisites in one line. If this process cannot be completed, output 0.

Example Input

4 4
2 1
3 1
1 4
4 3

Example Output

0

3. Approach to Problem Solving

This problem can be solved using the topological sorting algorithm. Generally, the topological sorting algorithm can be executed in two ways:

  • Method using DFS (Depth-First Search)
  • Method using BFS (Breadth-First Search) and in-degrees

4. BFS-based Topological Sorting

Let’s examine how to solve the problem using the BFS method for topological sorting. This approach calculates the ‘in-degrees’ and inserts the vertices with an in-degree of 0 into the queue. Then, it proceeds by taking one vertex from the queue at a time and decreasing the in-degrees of the other connected vertices.

4.1. Steps of the Algorithm

  1. Create an array to store in-degrees.
  2. Analyze each edge and calculate the in-degrees.
  3. Insert vertices with in-degrees of 0 into the queue.
  4. Remove vertices from the queue and decrease the in-degrees of the connected vertices.
  5. If all vertices have been processed, return the result; if there are unprocessed vertices, return 0.

5. Swift Implementation

Below is the code implementing the given problem in Swift.


import Foundation

func topologicalSort(N: Int, edges: [(Int, Int)]) {
    var graph = [[Int]](repeating: [Int](), count: N + 1)
    var inDegree = [Int](repeating: 0, count: N + 1)

    for edge in edges {
        let (a, b) = edge
        graph[a].append(b)
        inDegree[b] += 1
    }

    var result = [Int]()
    var queue = [Int]()

    // Insert vertices with in-degrees of 0 into the queue
    for i in 1...N {
        if inDegree[i] == 0 {
            queue.append(i)
        }
    }

    while !queue.isEmpty {
        let current = queue.removeFirst()
        result.append(current)

        for neighbor in graph[current] {
            inDegree[neighbor] -= 1
            if inDegree[neighbor] == 0 {
                queue.append(neighbor)
            }
        }
    }

    if result.count == N {
        print(result.map { String($0) }.joined(separator: " "))
    } else {
        print(0)
    }
}

let N = 4
let M = 4
let edges = [(2, 1), (3, 1), (1, 4), (4, 3)]

topologicalSort(N: N, edges: edges)

6. Conclusion

Topological sorting is a useful algorithm that is applied in many fields. In particular, it serves as a powerful tool through the concept of prerequisites when tasks need to be performed sequentially. Through this problem, I hope you learn the topological sorting algorithm and develop the ability to apply it to various problems. Similar problems frequently appear in coding tests, so ample practice is necessary.