PSTAT 10 Data Science Principles

Lecture 5: Algorithms

John Robin Inston

University of California, Santa Barbara

August 22, 2026

Introduction

๐Ÿ” Review: Lecture 4

๐Ÿ‘ˆ Last lecture we explored some automation methods such as:

  • Functions
  • Branching (if, else, else if)
  • Looping:
    • For Loops
    • While Loops
    • Repeat & Break Loops
  • Control Flow

๐Ÿ‘€ Outline: Lecture 5

๐Ÿ‘‡ This lecture we will shift our focus to problem solving, including:

  • Algorithmic Thinking
  • Branching Logic
  • Iterative Logic
  • Recursive Logic

Algorithms

๐Ÿ“Š Data Science

What is Data Science?

  • Data science is a combination of subjects including:
    • โž• Math
    • ๐Ÿ“Š Statistics
    • ๐Ÿ‘จโ€๐Ÿ’ป Programming
    • ๐Ÿ’ป Computer Science
    • ๐Ÿค– Machine Learning

Data All Around Us

  • Data is the single most prolific commodity in the world with over 1 trillion GB being generated per day.
  • This is far too much for any human to handle, so we are required to develop automated procedures using algorithms.

๐Ÿงฎ What is an Algorithm?

Algorithm Visualization

Algorithm

An algorithm is a process / set of rules performed sequentially to solve some kind of problem.

  • The rules are often mathematical but can be thought of as general functions.
  • We have all used algorithms before, particularly in math classes whenever we learned to solve a problem in several steps: calculus and integral rules, long division, algebra etc.

๐ŸŒณ Branching Algorithms

Branching Algorithm

Branching algorithms can be visualized as a case-dependent tree structure, where each branch determines which steps the algorithm applies.

Branching Algorithm

Example โ€” Branching Algorithm

Checking the length of a character string

  • Here is a simple example of a branching algorithm that checks that the length of a character string is greater than 5 characters.
  • If the string is greater than 5 characters, the algorithm returns TRUE, otherwise it returns FALSE.
# Define the algorithm 
check_string_length <- function(string) {
  if (nchar(string) > 5) {
    return(TRUE)
  } else {
    return(FALSE)
  }
}
# Test the algorithm
check_string_length("Hello world!")
[1] TRUE
  • For more complicated logic, we can use nested branching to check multiple conditions in a single algorithm.

๐Ÿช† Nested Branching Statements

Branching statements can be nested inside one another โ€” an if / else if / else block can contain another complete branching block.

  • This is useful when a decision depends on more than one condition, checked one at a time.
  • The general structure looks like:
if (logical1) {
  if (logical2) {
    ACTION A
  } else {
    ACTION B
  }
} else {
  ACTION C
}

Syntax Tips

  • Indent each nested level further than the block containing it โ€” this makes the structure easy to read at a glance.
  • Every opening brace { needs a matching closing brace } โ€” close the inner block before the outer one.
  • RStudio auto-indents and highlights matching braces for you โ€” use this to check your work!

๐Ÿช† Example โ€” Nested Branching

Suppose we want to classify a temperature, but โ€œhotโ€ vs โ€œcoolโ€ means something different depending on whether itโ€™s a weekday or a weekend:

classify_day <- function(temp, weekday) {
  if (weekday) {
    if (temp > 75) {
      return("Hot workday")
    } else {
      return("Cool workday")
    }
  } else {
    if (temp > 85) {
      return("Hot weekend")
    } else {
      return("Cool weekend")
    }
  }
}
classify_day(80, TRUE)
[1] "Hot workday"

๐Ÿ“Œ Notice the outer branch picks weekday vs weekend, and the inner branch then checks the temperature within it โ€” exactly the pattern youโ€™ll need for the next exercise!

๐Ÿ’ช Exercise โ€” Nested Branching Practice

04:00

Write a function check_number that takes a single numeric input x and uses nested branching as follows:

  1. First check whether x is positive or non-positive (zero or negative).
  2. If x is positive, nest a second branch that checks whether x is even or odd, returning "positive even" or "positive odd".
  3. If x is non-positive, nest a second branch that checks whether x is zero or negative, returning "zero" or "negative".

Hint: the modulo operator %% is helpful for checking even / odd โ€” recall x %% 2 == 0 when x is even.

โœ… Solution โ€” Nested Branching Practice

check_number <- function(x) {
  if (x > 0) {
    if (x %% 2 == 0) {
      return("positive even")
    } else {
      return("positive odd")
    }
  } else {
    if (x == 0) {
      return("zero")
    } else {
      return("negative")
    }
  }
}
check_number(4)
check_number(-7)
check_number(0)
[1] "positive even"
[1] "negative"
[1] "zero"

๐Ÿ“Œ Notice the outer branch (positive vs. non-positive) is fully resolved โ€” including both its nested branches closed off with } โ€” before the else for the outer branch even appears.

โœ… Solution โ€” A Better Approach

To demonstrate that there are various ways to accomplish the same task we introduce the function %in%, which tests whether a value is in a vector.

3 %in% c(1, 2, 3, 4, 5) # TRUE
[1] TRUE
6 %in% c(1, 2, 3, 4, 5) # FALSE 
[1] FALSE
# more efficient solution using %in%
word_search <- function(language, type) {
  # Name the matrix indices
  dimnames(lang_matrix) <- list(c("english", "spanish", "french"),
                                c("greeting", "farewell", "friend"))
  # Use named indices
  if(
    language %in% c("english", "spanish", "french") &
    type %in% c("greeting", "farewell")
  ) {
    return(lang_matrix[language, type])
  } else {
    print("Error: Word not found!")
  }
}

๐Ÿ” Iterative Algorithms

Inefficiency of Branching Algorithms

Branching algorithms are often excellent choices but they come with some limitations:

  • Larger numbers of branches are time consuming to program manually.
  • Inefficient for traversing objects of indeterminate length.
# Example of inefficient branching algorithm
bad_in_vector <- function(v) {
  if (v[1] == 1) {print("1 is in the vector")}
  else if (v[2] == 1) {print("1 is in the vector")}
  else if (v[3] == 1) {print("1 is in the vector")}
  else if (v[4] == 1) {print("1 is in the vector")}
  else if (v[5] == 1) {print("1 is in the vector")}
  else if (v[6] == 1) {print("1 is in the vector")}
  else if (v[7] == 1) {print("1 is in the vector")}
  else {print("1 is not in the vector")}
}

What are Iterative Algorithms?

Iterative Algorithm

An iterative algorithm is a process that repeats a set of instructions until a specific condition is met.

๐Ÿ” Example โ€” Iterative Algorithm

Suppose we want to count how many negative numbers appear in a vector. An iterative algorithm repeats the same check for every element:

count_negatives <- function(v) {
  count <- 0
  for (i in 1:length(v)) {
    if (v[i] < 0) {
      count <- count + 1
    }
  }
  return(count)
}
count_negatives(c(3, -2, -7, 5, -1))
[1] 3

๐Ÿ“Œ Notice the pattern: loop over every index, check a condition, then update something โ€” this is exactly the structure youโ€™ll use in the next exercise!

๐Ÿ’ช Exercise โ€” Iterative Algorithms

03:00

Write a function alter_entry that:

  1. Takes a vector (of any data type) as input, and
  2. Returns the same vector, but replacing every instance of the number 7 in the vector with 0.

Below are some examples of the function output:

alter_entry(c(7,2,3,7,1))
alter_entry(c(10, 7, 3, 7, 8))
[1] 0 2 3 0 1
[1] 10  0  3  0  8

โœ… Solution โ€” Iterative Algorithms

  1. First we construct the surrounding function.

  2. Then we define the for loop iterating over the indices of the input vector.

  3. Each iteration checks whether the value at the current index is equal to 7. If it is, we replace it with 0.

# define alter_entry function
alter_entry <- function(v) {
  for(i in 1:length(v)) {
    if(v[i] == 7){
      v[i] <- 0
    }
  }
  return(v)
}
alter_entry(c(7,2,3,7,1))
alter_entry(c(10, 7, 3, 7, 8))
[1] 0 2 3 0 1
[1] 10  0  3  0  8
  • Remember you should still always try to avoid looping when possible.
    • Lets see if we can improve the efficiency of our previous function by introducing the which() function.

๐Ÿ”Ž The which() Function

A very helpful function!

  • Often we would like to return the indices of the elements of a vector that satisfy a certain condition.
    • For this we can use the which() function.
    • The which() function takes a logical vector as input and returns the indices of the TRUE values.
    • It can be helpful for avoiding iterative logical queries!
  • An alternative solution to our previous example using the which() function could be:
alter_entry <- function(v) {
  v[which(v == 7)] <- 0
  return(v)
}

๐Ÿ“Œ Which function is more efficient?

๐ŸŒ€ Fibonacci Sequence

In preparation for the next exercise we briefly introduce the Fibonacci sequence.

The Fibonacci sequence is a mathematical pattern occurring commonly in nature. It appears in the golden ratio, which is found in petals, seashells, and galaxy spirals.

Golden Ratio

The sequence itself is constructed by starting with the initial values \(0\) and \(1\), and every subsequent value is the sum of the previous two, i.e.

\[ x_n = x_{n-1} + x_{n-2}; \quad x_1 = 0, \quad x_2 = 1. \]

๐Ÿ’ช Exercise โ€” Fibonacci Sequence

05:00

Write an iterative function fib_n that returns the \(n\)-th term of the Fibonacci sequence:

  1. The function should take one numerical scalar input n.
  2. The function should return a numeric value representing the \(n\)-th term of the Fibonacci sequence.
  3. Ignore exception cases and do not worry about robustness โ€” assume the input will always be a positive integer.
fib_n(10)
fib_n(25)
[1] 34
[1] 46368

โœ… Solution โ€” Fibonacci Sequence

  1. We start once again by defining the function and its input.

  2. Next we define the base cases for the first two terms of the Fibonacci sequence.

  3. Finally we define a for loop that iteratively calculates the Fibonacci sequence up to the \(n\)-th term.

# define fib_n function
fib_n <- function(n){
  if(n == 1) {
    return(0)
  } else if (n==2) {
    return(1)
  } else {
    temp <- c(0,1)
    for(i in 3:n){
      temp[i] <- temp[i-1] + temp[i-2]
    }
    return(temp[n])
  }
}
fib_n(10)
fib_n(25)
[1] 34
[1] 46368

๐Ÿงฉ Combining Everything

Notice that in our previous example we were required to use both branching and iteration. It is common for algorithms to require the use of several tools.

  • To understand and eventually design algorithms it is important to know how the individual pieces / steps work and what they are doing.

Certain tricks come up more often than others, such as:

  • Iterating through vector indices (enumeration);
  • Storing and updating temporary or intermediate values; and
  • Utilizing data structure and specific behavior.

Letโ€™s see if we can combine these tools to solve a problem in the next exercise!

๐Ÿ’ช Exercise โ€” Last Negative

05:00

Combine some of the tools we have been studying by defining a function called last_negative that:

  1. Takes a numeric vector v.
  2. Returns the last (index-wise) negative value in v and its index (in a 2-element vector).
  3. If v does not have any negative values, returns the character string "No negative values".

Below are some examples of the function output:

last_negative(c(-1,2,6,-2,8,-9))
last_negative(c(5,2,1,4,5,6,9))
[1] -9  6
[1] "No negative values"

โœ… Solution โ€” Last Negative

  1. We start by defining the function and its input.

  2. Next we define a temporary variable temp to store the last negative value and its index, or a message if no negative values are found.

  3. Finally we define a for loop that iterates through the vector, checking for negative values and updating temp accordingly.

last_negative <- function(v) {

  temp <- "No negative values" # storing tempory vector

  for(i in 1:length(v)){
    if(v[i] < 0) {
      temp <- c(v[i], i)
    }
  }
  return(temp)
}
last_negative(c(-1,2,6,-2,8,-9))
last_negative(c(5,2,1,4,5,6,9))
[1] -9  6
[1] "No negative values"

Nested Loops

๐Ÿช† Nested Loops

Sometimes we may wish to run loops inside other loops (e.g. iterating over first the rows and then the columns of a matrix), and to do so we use nested loops.

Example Nested For Loop

for(LOGICAL1, often index-based) {
  DO OPERATION1

  for(LOGICAL2, often index-based){
    DO OPERATION2
  }
}
  • Nested loops are useful but slow!
  • Good practice is to only use them when necessary, prioritising vectorized solutions.

๐Ÿ’ช Exercise โ€” Two Sum

05:00

Write a function called two_sum that:

  1. Takes an input vector v and a numerical scalar target, and
  2. Returns the indices of the two entries of v that add up to the target.

Assume that:

  • Two indices cannot be equal (i.e. target will not be twice any value in v);
  • If no values summing to target are found, return NULL; and
  • Only one such pair of elements in v sum to the target.
two_sum(c(5,6,0,1,4),11)
[1] 1 2

โœ… Solution โ€” Two Sum

# define two_sum function
two_sum <- function(v, target) {
  n <- length(v)
  for (i in 1:(n - 1)) {
    for (j in (i + 1):n) {
      if(v[i] + v[j] == target) {
        return(c(i, j))
      }
    }
  }
return(NULL)
}

๐Ÿ“Œ See whether you can design a more efficient function using vectors!

Searching & Sorting

๐Ÿ” Searching & Sorting

Famous Problems

Two very famous problems in computer science are:

  1. How to search data structures for specific values; and
  2. How to sort unsorted numerical vectors.

R has built in functions to handle both of these problems (i.e. sort() and %in%) but to understand the challenges of these problems we take a look at two specific solutions:

  1. Bubble sorting
  2. (Iterative) binary searching



Sorting.

๐Ÿซง Bubble Sort

In 2007, former Google CEO Eric Schmidt asked then presidential candidate Barack Obama during an interview about the best way to sort one million integers.

Obama paused for a moment and replied โ€œI think the bubble sort would be the wrong way to go.โ€

Bubble Sorting Visualization

๐Ÿซง The Bubble Sort Algorithm

Main Idea

  • Assume our input is a (potentially long) vector v of unsorted values.
  • The idea is that we let the โ€œlighterโ€ values โ€œsinkโ€ to the bottom.
  • Apply the following algorithm:
    • Iterate through the entire vector.
    • For every pair of indices j and j+1, if v[j] > v[j+1] swap the values at those indices.
    • Stop iterating when the vector is fully sorted.
    • The desired output is a vector containing the sorted values of v.

Example โ€” Defining Bubble Sort

Implementation

bubble_sort <- function(v) {
  n <- length(v)
  for (i in 1:(n - 1)) { # iterate through n-1 loops
    is_swapped <- FALSE
      for (j in 1:(n - i)) { # swap values at specific indices
        if (v[j] > v[j + 1]) {
          temp <- v[j]
          v[j] <- v[j + 1]
          v[j + 1] <- temp
          is_swapped <- TRUE
        }
      }
    if (!is_swapped) { # break out of loop if fully sorted
      break
    }
  }
return(v)
}

Testing the Algorithm

As an example of our algorithm in action letโ€™s define a vector of 30 numbers in a random order:

bubble_sort_test <- sample(1:100, 30, replace = FALSE)
bubble_sort_test
 [1]   3  14  10  25  30  47  79  32  67  29  28  92 100  22   4  16  94  37  46
[20]  35  50  52  12  13  90  95  55  53   5  59

We apply the bubble sort algorithm:

bubble_sort(bubble_sort_test)
 [1]   3   4   5  10  12  13  14  16  22  25  28  29  30  32  35  37  46  47  50
[20]  52  53  55  59  67  79  90  92  94  95 100

๐ŸŽฏ Binary Search Algorithm

Implementation

# define binary search algorithm
binary_search <- function(v, target) {
  left <- 1
  right <- length(v)
  while (left <= right) {
    mid <- floor((left + right) / 2) # check the vector midpoint
    if (v[mid] == target) {
      return(mid)
    } else if (v[mid] < target) { # check lower
      left <- mid + 1
    } else {
      right <- mid - 1
    }
  }
  return(NULL)
}

Binary Search Example

# define a sorted vector
test_vector <- c(1, 3, 5, 7, 9, 11, 13, 15, 17, 19)
binary_search(test_vector, 7) # returns 4
[1] 4

๐Ÿ”„ Recursion

Recursion is another common algorithmic pattern.

  • It is the act of defining a problem in terms of simpler versions of itself.
  • Recursion consists of a base case and a recursive step.
# recursion general form
recursion <- function(INPUTS) {
  if (TERMINATION CONDITION) { # base case
    END RECURSIVE FUNCTION
  } else {
    new_inputs <- DO SOMETHING # recursive step
    recursion(new_inputs)
  }
}

๐ŸŒ€ Example โ€” Recursive Fibonacci

Recall that we previously wrote an iterative Fibonacci algorithm.

  • Iterative approaches are effective, and our previous function worked perfectly.
  • However, iterative setups can be complicated to implement.
  • Furthermore, the main drawback of iteration is typically computational cost and time โ€” it is often not very efficient!

Letโ€™s try using recursion to create a new version of the fib_n function.

  • We have the same setup: take an input \(n\) and return the \(n\)-th term of the Fibonacci sequence.
  • This time, construct the function using recursion.

๐ŸŒ€ Recursive Fibonacci โ€” Implementation

# recursive fibonacci function
fib_recur <- function(n) {
  if (n <= 2) { # base case
    return(n-1)
  }
  return(fib_recur(n - 1) + fib_recur(n - 2)) # recursive step
}
# example output
fib_recur(25)
[1] 46368

๐Ÿ“Œ The recursive Fibonacci function works exactly the same, however the implementation is much faster. The drawback is that the logic behind the implementation is less intuitive.

Summary

โœ… Topics Covered

๐Ÿค” Today we began exploring problem solving:

  • Algorithmic Thinking
  • Branching Logic
  • Iterative Logic
  • Recursive Logic

๐Ÿ“… Next Class

๐Ÿคฉ Next class we look at:

  • Storing data
  • Factor data