Question about variable scope with while loops

project
ruby

#1

I am trying to design a small program to print the third greatest number from a provided array of numbers. I thought I had designed it to assign the largest of two numbers to a global variable called 'the_greatest', and other to one called 'second_greatest', or to a third called 'the_third_greatest', then another iteration of the while loop would compare again and find the appropriate numbers. However, I ran the method and it is printing out 0's(the default values for the variables before the while loop). My only conclusion is that the variables in the while loop cannot change the values of the same variables outside of the while loop. If this is the case, why?

Here is my code: Thanks!

def third_greatest(nums)
  idx = 0
  idx2 = idx + 1
  the_greatest = 0
  second_greatest = 0
  the_third_greatest = 0
  while idx < nums.length
  if nums[idx] > nums[idx2]
    greatest = nums[idx]
      if greatest > the_greatest
      the_greatest = greatest
      elsif
       greatest > second_greatest
       second_greatest = greatest
      elsif
       greatest > the_third_greatest
        the_third_greatest = greatest
      end
      idx += 1
  else
    greatest = nums[idx2]
      if greatest > the_greatest
      the_greatest = greatest
      elsif
       greatest > second_greatest
       second_greatest = greatest
      elsif
       greatest > the_third_greatest
        the_third_greatest = greatest
      end
      idx += 1
  end
  end
  puts the_third_greatest
end

puts third_greatest([1,2,3,4])

#2

scope is not the problem here, look at this code:

def third_greatest(nums)
  idx = 0
  idx2 = idx + 1
  the_greatest = 0
  second_greatest = 0
  the_third_greatest = 0
  while idx < nums.length
  if nums[idx] > nums[idx2]
    greatest = nums[idx]
      if greatest > the_greatest
      the_greatest = greatest
      elsif
       greatest > second_greatest
       second_greatest = greatest
      elsif
       puts "i get here"
       greatest > the_third_greatest
        the_third_greatest = greatest
      end
      idx += 1
  else
    greatest = nums[idx2]
      if greatest > the_greatest
      the_greatest = greatest
      elsif
       greatest > second_greatest
       second_greatest = greatest
      elsif
       puts "i get somewhere" 
       greatest > the_third_greatest
        the_third_greatest = greatest
        
      end
      idx += 1
  end
  end
  puts the_third_greatest
end

puts third_greatest([1,2,3,4])

i put puts statements where the_third_greatest is defined, not one of them print, so the reason the_third_greatest returns zero, is because you give a value of zero (in the beginning), and nowhere it gets changed. Isn't a better idea to simple sort the list, and then the last (nums[-1]), second to last (nums[-2]) an third last to last element (nums[-3]), or the first, depending on how you sort


#3

Hi @stetim94 ,

In this elseif block, the placement of the puts statement makes it serve as the condition that controls the block.

      elsif
       puts "i get somewhere" 
       greatest > the_third_greatest
        the_third_greatest = greatest
        
      end

Since puts returns a nil, the condition would act as a false if the statement were reached, regardless of the value of any of the variables in the program.


#4

I guess i haven't coded in ruby for a while, hold on, if you have this:

elsif
  greatest > the_third_greatest

greatest > the_third_greatest is condition checked of the elif statement?


#5

Yes, that would make this the condition:

greatest > the_third_greatest

#6

Let's say i change the puts statements:

def third_greatest(nums)
  idx = 0
  idx2 = idx + 1
  the_greatest = 0
  second_greatest = 0
  the_third_greatest = 0
  while idx < nums.length
  if nums[idx] > nums[idx2]
    greatest = nums[idx]
      if greatest > the_greatest
      the_greatest = greatest
      elsif
       greatest > second_greatest
       second_greatest = greatest
      elsif
       greatest > the_third_greatest
        the_third_greatest = greatest
        puts "i get here"
      end
      idx += 1
  else
    greatest = nums[idx2]
      if greatest > the_greatest
      the_greatest = greatest
      elsif
       greatest > second_greatest
       second_greatest = greatest
      elsif
       greatest > the_third_greatest
        the_third_greatest = greatest
        puts "i get here"
      end
      idx += 1
  end
  end
  puts the_third_greatest
end

puts third_greatest([1,2,3,4])

none of the conditions where the_third_greatest gets assigned a value after getting a value of 0 in the beginning becomes true. Or did i miss something else this time?


#7

The original algorithm is buggy. Try this one:

def third_greatest(nums)
  idx = 0
  the_greatest = 0
  the_second_greatest = 0
  the_third_greatest = 0
  while idx < nums.length
    if nums[idx] >= the_greatest
      the_third_greatest = the_second_greatest
      the_second_greatest = the_greatest
      the_greatest = nums[idx]
    elsif nums[idx] >= the_second_greatest
      the_third_greatest = the_second_greatest
      the_second_greatest = nums[idx]
    elsif nums[idx] >= the_third_greatest
      the_third_greatest = nums[idx]
    end
    puts "The item is " + nums[idx].to_s
    puts "Greatest is now " + the_greatest.to_s
    puts "Second greatest is now " + the_second_greatest.to_s
    puts "Third greatest is now " + the_third_greatest.to_s
    puts
    idx += 1
  end
  return the_third_greatest
end

puts "Result: " + third_greatest([1, 8, 4, 6, 7, 9, 3, 5, 2]).to_s

Output:

The item is 1
Greatest is now 1
Second greatest is now 0
Third greatest is now 0

The item is 8
Greatest is now 8
Second greatest is now 1
Third greatest is now 0

The item is 4
Greatest is now 8
Second greatest is now 4
Third greatest is now 1

The item is 6
Greatest is now 8
Second greatest is now 6
Third greatest is now 4

The item is 7
Greatest is now 8
Second greatest is now 7
Third greatest is now 6

The item is 9
Greatest is now 9
Second greatest is now 8
Third greatest is now 7

The item is 3
Greatest is now 9
Second greatest is now 8
Third greatest is now 7

The item is 5
Greatest is now 9
Second greatest is now 8
Third greatest is now 7

The item is 2
Greatest is now 9
Second greatest is now 8
Third greatest is now 7

7

#8

I know the code is buggy, but i am not going to change someones code completely


#9

Your revised code is pretty brilliant. It clearly works well. I'm building this program to solve a problem set and the solution is actually very similar to what you have provided.

But, still, I wonder why my code does not work the same way. I understand that 'elsif' for 'the_third_greatest' never produces any output but why?

From a logical standpoint, it seems like my code should work. Here are my annotations:

def third_greatest(nums)
idx = 0
idx2 = idx + 1
the_greatest = 0
second_greatest = 0
the_third_greatest = 0

while idx < nums.length # For the current number
if nums[idx] > nums[idx2] # If the current number is larger than the next number
greatest = nums[idx] #(new variable) greatest, is set to current number
if greatest > the_greatest # If current number is larger than the greatest overall number (0)
the_greatest = greatest # then the new greatest overall number is set to current number
(which should happen because any number will be larger than 0)
elsif # otherwise, if current number is larger than second_greatest(0), setsecond_greatest to current number
greatest > second_greatest
second_greatest = greatest
elsif # if current is not greater than the previous 2 conditions, set the_third_greatest to current number
greatest > the_third_greatest
the_third_greatest = greatest
end
idx += 1 # reloop and compare the next 2 numbers
else Repeat previous logic to the SECOND number in the above comparison
greatest = nums[idx2]
if greatest > the_greatest
the_greatest = greatest
elsif
greatest > second_greatest
second_greatest = greatest
elsif
greatest > the_third_greatest
the_third_greatest = greatest
end
idx += 1
end
end
puts the_third_greatest # By this point, the_third_greatest should be set to whichever number was NOT greater than the_greatest or second_greatest but still greater than the_third_greatest on every iteration
end

So what's wrong with that code?


#10

Hi @veryscarycary ,

The first two lines within your function are ...

  idx = 0
  idx2 = idx + 1

That assigns 1 to idx2, which refers to the second item in the nums array when you use it as an index. As the while loop iterates, the value of idx2 remains 1, and you use it here ...

if nums[idx] > nums[idx2]

... and here ...

greatest = nums[idx2]

What I don't understand about the algorithm is the significance of the second item in the array, as compared to the significance of the other items.


#11

Oh!!! I did not realize that idx2 remained with a value of 1 during the loop. I figured that by increasing idx by 1, it would increase idx2 by 1 as well! Doh! But now I can see that i would need to include that in the while loop for it to apply again. Thanks.

As far as the second item in the nums array, I assume you mean nums[idx2], it was my way of being able to conceptualize how a computer might think about finding the greatest of a group of numbers. I often times think "How can I write this?" and it seems I often arrive at a place that compares one "thing" to another "thing". So I ended up comparing idx with idx2 and judging which was greater and moving down the line. But now I realize that there may be a simpler way of writing it.... :sweat_smile:


#12

I'm currently running through a list of practice problems provided by a bootcamp called App Academy in San Francisco. Apparently, it's very competitive and has a 5% acceptance rate so I need to be able to write small programs like this one on the fly and be competent enough to complete their admission test in under 45 minutes for me to be admitted. I hope I can get the hang of it. :disappointed_relieved:


#13

Hi @veryscarycary ,

Thus far, we have assumed that the array will have no negative numbers, since we initialize the three variables that keep track of the ranks to 0. Is that a fair assumption, or do you want the function to work for arrays where there is no assumed upper or lower limit to the range values it contains?


#14

You are correct. The problem does not explicitly state that I should assume the numbers won't be negative, so it would be advantageous to add this range. In the case that someone provides a set of negative numbers, the method would provide 0 as the third greatest number even if it wasn't provided. =/


#15

Hi @veryscarycary ,

Here's a suggestion for allowing for negative and positive numbers of any magnitude ...
You can begin by setting the three ranking variables to a number that is low enough so that it would be equalled or exceeded by at least three numbers in the array. Currently, you set each of these variables to 0. Instead, you could initialize them all to the lowest of the first three numbers in the array. This would guarantee that the greatest, the second-greatest, and the third-greatest numbers in the array would replace them, if necessary, as you iterated through the array to perform comparisons.


#16

For the purpose of bringing some Python enthusiasts into the discussion, here's a Python solution to the problem:

import random

def third_greatest(nums, verbose = False):
    """Returns the third-greatest item in nums;
    nums must contain at least three items."""
    # initialize the rank list with the min of the first three items in nums
    rank = [min(nums[:3])] * 3
    # largest at index 0
    # second largest at index 1
    # third largest at index 2
    for num in nums:
        if num >= rank[0]:
            rank[0], rank[1], rank[2] = num, rank[0], rank[1]
        elif num >= rank[1]:
            rank[1], rank[2] = num, rank[1]
        elif num >= rank[2]:
            rank[2] = num
        if verbose:
            print("Found item: %s" % (num))
            print("Rankings: %s %s %s" % (rank[0], rank[1], rank[2]))
    return rank[2]

nums = [random.randint(-10, 10) for i in range(12)]
print(nums)
print("The third-greatest number in the list is: %s" % (third_greatest(nums, True)))

#17

This is a really dumb question, given the above discussion. How might we apply this?

x = [2,5,8,4,6,2,9,8,1,3,7,4]  # a list with duplicates
y = set(x)                     # gives an ordered list with no duplicates
z = list(y)[-1::-1][2]         # gives the third largest value in the list

Perhaps I am oversimpifying.

Edit: fixed typo


#18

Hi, @mtf ,

It works if this ...

z = list(x)[-1::-1][2]

... is changed to this ...

z = list(y)[-1::-1][2]

So, these two lines eliminate duplicates, sort the list in reverse order, and access the third item in the list, which is the item we want ...

y = set(x)
z = list(y)[-1::-1][2]

Perhaps @veryscarycary can let us know whether the problem set he is trying to solve specifies whether or not duplicates should be preserved in the solution. For instance, if the list were [9, 8, 9, 7, 4], would the third-greatest number be 8 or 7?

Either way, we, of course, do need to perform some sorting in order to solve this problem, whether it be through the above, or through using an explicit sort. We could, of course, do this ...

y = set(x)
z = sorted(y)[-3]
print(z)

This brings up the issue of efficiency and complexity of algorithms. For small lists, using a sort seems fine. But, if we might be working with large lists, should we sort the entire list to find the number? This probably has a complexity of O(n log n).

It might be more efficient if, instead, we just perform one traversal of the list, keeping track of the three greatest numbers thus far encountered as we go. There is still some sorting going on if we do this, but the actual sorting really only involves the three highest numbers. So, I think the complexity of this strategy is O(n).


#19

Sorry to keep posting, but this topic brings up a lot of interesting issues.

Since a set is unordered, there is not a guarantee that converting a list of numbers to a set, then converting that set back to a list, will sort the numbers. Items in a set are stored in "buckets", based on a hash function that uses the values of the items to compute hash codes. For small numbers, this process might preserve an ordering, but since the number of buckets is finite, the hash codes of large numbers might not always reflect the magnitudes of the numbers.

Here's the result of an experiment, executed in Python 3:

x = [22222222,
     55555555,
     88888888,
     44444444,
     66666666,
     22222222,
     99999999,
     88888888,
     11111111,
     33333333,
     77777777,
     44444444]
y = set(x)
z = list(y)
print(z)
print("Result:")
print(z[-1::-1][2])

Output:

[55555555, 11111111, 66666666, 22222222, 77777777, 33333333, 88888888, 44444444, 99999999]
Result:
88888888

With the large numbers, the list did not get sorted as we had hoped, and we got the wrong result.

A set can be thought of as a dictionary that contains keys, but no associated values. In a dictionary, the hash function is applied to the keys, but in a set the hash function is applied to the values.

For detailed information, see Wikipedia: Hash function.


#20

Very useful and important insight, and the sort we need. Thank you for taking the time to elaborate.