Introducing Stacks, Queues, and HashSets

In the last chapter, we revisited variables, types, and classes to see what they had to offer beyond the basic features introduced at the beginning of the book. In this chapter, we'll take a closer look at new collection types and learn about their intermediate-level capabilities. 

Each of the new collection types in this chapter has a specific purpose. For most scenarios where you need a collection of data, a list or array works just fine. When you need temporary storage or control over the order of collection elements, or more specifically, the order they are accessed, look to stacks and queues. When you need to perform operations that depend on every element in a collection to be unique, meaning not duplicated, look to HashSets. Remember, being a good programmer isn't about memorizing code: it's about choosing the right tool for the right job. 

Before you start on the code in the following section, let's lay out the topics you'll be learning about:

  • Introducing stacks
  • Peeking and popping elements
  • Common methods
  • Working with queues
  • Adding, removing, and peeking elements
  • Using HashSets
  • Performing operations

Introducing stacks

At its most basic level, a stack is a collection of elements of the same specified type. The stack length is variable, meaning it can change depending on how many elements it's holding. The important difference between a stack and a list or array is how the elements are stored. Stacks follow the last-in-first-out (LIFO) model, meaning the last element in the stack is the first accessible element. This is useful when you want to access elements in reverse order. You should note that they can store null and duplicate values.

All the collection types in this chapter are a part of the System.Collections.Generic namespace, meaning you need to add the following code to the top of any file that you want to use them in:

using System.Collections.Generic;

Now that you know what you're about to work with, let's take a look at the basic syntax for declaring stacks.

Basic syntax

A stack variable declaration needs to meet the following requirements:

  • The Stack keyword, its element type between left and right arrow characters, and a unique name
  • The new keyword to initialize the stack in memory, followed by the Stack keyword and element type between arrow characters
  • A pair of parentheses capped off by a semicolon

In blueprint form, it looks like this:

Stack<elementType> name = new Stack<elementType>();

Unlike the other collection types you've worked with, stacks can't be initialized with elements when they're created. 

C# supports a non-generic version of the Stack type that doesn't require you to define the type of element in the stack:

Stack myStack = new Stack();

However, this is less safe and more costly than using the preceding generic version. You can read more about Microsoft's recommendation at https://github.com/dotnet/platform-compat/blob/master/docs/DE0006.md.

Your next task is to create a stack of your own and get hands-on experience with working with its class methods.

Time for action – storing collected items

To test this out, you're going to modify the existing item collection logic in Hero Born by using a stack to store possible loot that can be collected:

  1. Open GameBehavior.cs and add in a new stack variable named lootStack:
// 1
public
Stack<string> lootStack = new Stack<string>();
  1. Update the Initialize method with the following code to add new items to the stack:
public void Initialize() 
{
_state = "Manager initialized..";
_state.FanceyDebug();
Debug.Log(_state);

// 2
lootStack.Push("Sword of Doom");
lootStack.Push("HP+");
lootStack.Push("Golden Key");
lootStack.Push("Winged Boot");
lootStack.Push("Mythril Bracers");
}
  1. Add a new method to the bottom of the script to print out the stack information:
// 3
public void PrintLootReport()
{
Debug.LogFormat("There are {0} random loot items waiting for
you!", lootStack.Count);
}
  1. Open ItemBehavior.cs and call PrintLootReport from the gameManager instance:
void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.name == "Player")
{
Destroy(this.transform.parent.gameObject);
Debug.Log("Item collected!");
}

gameManager.Items += 1;

// 4
gameManager.PrintLootReport();
}

Breaking this down, it does the following:

  1. Creates an empty stack with elements of the string type
  2. Uses the Push method to add string elements to the stack, increasing its size each time
  3. Prints out the stack count whenever the method is called
  4. Calls PrintLootReport every time an item is collected by the player:

Now that you have a working stack, you're ready to experiment with how items are accessed using the stack class' Pop and Peek methods.

Popping and peeking

We've already talked about how stacks store elements using the LIFO method. Now, we need to look at how elements are accessed in a familiar but different collection type – by peeking and popping:

  • The Peek method returns the next item on the stack without removing it, letting you "peek" at it without changing anything.
  • The Pop method returns and removes the next item on the stack, essentially "popping" it off and handing it to you.

Both of these methods can be used by themselves or together depending on what you need. You'll get hands-on experience with both methods in the following section.

Time for action – the last item collected

Your next task is to grab the last item added to lootStack. In our example, the last element is determined programmatically in the Initialize method, but you can imagine how the elements could be randomized or added based on some parameter. Either way, update PrintLootReport with the following code:

public void PrintLootReport()
{
// 1
var currentItem = lootStack.Pop()

// 2
var nextItem = lootStack.Peek()

// 3
Debug.LogFormat("You got a {0}! You've got a good chance of finding
a {1} next!", currentItem, nextItem);


Debug.LogFormat("There are {0} random loot items waiting for you!",
lootStack.Count);
}

Here's what's going on:

  1. Calls Pop on lootStack, removes the next item on the stack, and stores it.
  2. Calls Peek on lootStack and stores the next item on the stack without removing it.
  1. Adds a new debug log to print out the item that was popped off and the next item on the stack.

You can see from the console that Mythril Bracers, the last item added to the stack, was popped off first, followed by Winged Boots, which was peeked at but not removed. You can also see that lootStack has four remaining elements that can be accessed:

Now that you know how to create, add, and query elements from a stack, we can move on to some common methods that you have access to through the stack class.

Common methods

First, you can use the Clear method to empty out or delete the entire contents of a stack:

// Empty the stack and reverting the count to 0
lootStack.Clear()

If you want to know whether an element exists in your stack, use the Contains method and specify the element you're looking for:

// Returns true for "Golden Key" item
var itemFound = lootStack.Contains("Golden Key");

If you need to copy the elements of a stack to an array, the CopyTo method will let you specify the destination and the starting index for the copy operation:

// Copies loot stack items to an existing array starting at the 0 index
string[] copiedLoot = new string[lootStack.Count];
numbers
.CopyTo(copiedLoot, 0);

If you need to convert a stack into an array, simply use the ToArray method. This conversion creates a new array out of your stack, which is different than the CopyTo method, which copies the stack elements to an existing array.

You can also convert a stack into a string if needed by using the ToString method:

// Copies an existing stack to a new array
lootStack.ToArray();

// Returns a string representing the stack object
lootStack.ToString();

If you're paying attention to good coding practices, you'll want to check whether there's the next item on your stack before popping or peeking at it. Luckily, the stack class has two methods for that exact scenario – TryPeek and TryPop:

// The item will NOT be removed from the stack.
bool itemPresent = lootStack
.TryPeek(out lootItem);
if(itemPresent)
Debug.Log(lootItem);
else
Debug.Log("Stack is empty.");

// The item WILL be removed from the stack
bool itemPresent = lootStack.TryPop(out lootItem);
if(itemPresent)
Debug.Log(lootItem);
else
Debug.LogFormat("Stack is empty.");

Both methods return a true or false value based on the presence of an object at the top of the stack. If there is an object present, it will be copied to the out result parameter and the method will return true. If the stack is empty, the out result will default to its initial value and the method will return false.

You can find the entire list of stack methods in the C# documentation at https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1?view=netcore-3.1.

That wraps up our introduction to stacks, but we're going to talk about its cousin, the queue, in the following section. 

Working with queues

Like stacks, queues are collections of elements or objects of the same type. The length of any queue is variable just like a stack, meaning its size changes as elements are added or removed. However, queues follow the first-in-first-out (FIFO) model, meaning the first element in the queue is the first accessible element. You should note that queues can store null and duplicate values but can't be initialized with elements when they're created. 

Basic syntax

A queue variable declaration needs to meet the following requirements:

  • The Queue keyword, its element type between left and right arrow characters, and a unique name
  • The new keyword to initialize the queue in memory, followed by the Queue keyword and element type between arrow characters
  • A pair of parentheses capped off by a semicolon

In blueprint form, a queue looks as follows:

Queue<elementType> name = new Queue<elementType>();
C# supports a non-generic version of the Queue type that doesn't require you to define the type of element it stores:

Queue myQueue = new Queue();

However, this is less safe and more costly than using the preceding generic version. You can read more about Microsoft's recommendation at https://github.com/dotnet/platform-compat/blob/master/docs/DE0006.md.

An empty queue all by itself isn't all that useful; you want to be able to add, remove, and peek at its elements whenever you need, which is the topic of the following section.

Adding, removing, and peeking

Since the lootStack variable in the previous sections could easily be a queue, we'll keep the following code out of our game scripts for efficiency. However, feel free to explore the differences, or similarities, of these classes in your own code.

To create a queue of string elements, use the following:

// Creates a new Queue of string values.
Queue<string> activePlayers = new Queue<string>();

To add elements to the queue, call the Enqueue method with the element you want to add:

// Adds string values to the end of the Queue.
activePlayers.Enqueue("Harrison");
activePlayers.Enqueue("Alex");
activePlayers.Enqueue("Haley");

To see the first element in the queue without removing it, use the Peek method:

// Returns the first element in the Queue without removing it.
var firstPlayer = activePlayers.Peek();

To return and remove the first element in the queue, use the Dequeue method:

// Returns and removes the first element in the Queue.
var firstPlayer = activePlayers.Dequeue();

Now that you know how to work with the basic features of a queue, feel free to explore the more intermediate and advanced methods that the queue class offers.

Common methods

Queues and stacks share almost the exact same features, so we won't go over them a second time. You can find a complete list of methods and properties in the C# documentation at https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1?view=netcore-3.1

Before closing out the chapter, let's take a look at the HashSet collection type and the mathematical operations it's uniquely suited for.

Using HashSets

The last collection type we'll get our hands on in this chapter is the HashSet. This collection is very different from any other collection type that we've come across: it cannot store duplicate values and is not sorted, meaning its elements are not ordered in any way. Think of HashSets as dictionaries with just keys, instead of key-value pairs. They can perform set operations and element lookups extremely fast, which we'll explore at the end of this section, and are best suited to situations where the element order and uniqueness are a top priority. 

Basic syntax

A HashSet variable declaration needs to meet the following requirements:

  • The HashSet keyword, its element type between left and right arrow characters, and a unique name
  • The new keyword to initialize the HashSet in memory, followed by the HashSet keyword and element type between arrow characters.
  • A pair of parentheses capped off by a semicolon.

In blueprint form, it looks as follows:

HashSet<elementType> name = new HashSet<elementType>();

Unlike stacks and queues, you can initialize a HashSet with default values when declaring the variable:

HashSet<string> people = new HashSet<string>();

// OR

HashSet<string> people = new HashSet<string>() { "Joe", "Joan", "Hank"};

To add elements, use the Add method and specify the new element:

people.Add("Walter");
people.Add("Evelyn");

To remove an element, call Remove and specify the element you want to delete from the HashSet:

people.Remove("Joe");

That's it for the easy stuff, and this should start to feel pretty familiar at this point in your programming journey. The set operations are where the HashSet collection really shines, which is the topic of the following section.

Performing operations

Set operations are performed on the calling collection object and the passed-in collection object. More specifically, set operations are used to modify the calling HashSet elements based on which operation is used. We'll get into this in more detail in the following code, but first, let's go over the three main set operations that crop up in programming scenarios the most often:

In the following definitions, currentSet refers to the HashSet calling an operation method and specifiedSet refers to the passed-in HashSet method parameter. The modified HashSet is always the current set:

currentSet.Operation(specifiedSet);

There are three main operations that we'll be working with in the rest of this section:

  • UnionWith adds the elements of the current and specified sets together.
  • IntersectWith stores only the elements that are in both the current and specified sets.
  • ExceptWith subtracts the elements of the specified set from the current set.
There are two more groups of set operations that deal with subset and superset computations, but these are targeted at specific use cases that are beyond the scope of this chapter. You can find all the relevant information for these methods at https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1?view=netcore-3.1.

Let's say we have two sets of player names – one for active players and one for inactive players:

HashSet<string> activePlayers = new HashSet<string>() { "Harrison",
"Alex", "Haley"};
HashSet<string> inactivePlayers = new HashSet<string>() { "Kelsey",
"Basel"};

We would use the UnionWith operation to modify a set to include all the elements in both sets:

activePlayers.UnionWith(inactivePlayers);
// activePlayers now stores "Harrison", "Alex", "Haley", "Kelsey",
"Basel"

Now, let's say we have two different sets – one for active players and one for premium players:

HashSet<string> activePlayers = new HashSet<string>() { "Harrison", 
"Alex", "Haley"};
HashSet<string> premiumPlayers = new HashSet<string>() { "Haley",
"Basel" };

We would use the IntersectWith operation to find any active players that are also premium members:

activePlayers.IntersectWith(premiumPlayers);
// activePlayers now stores only "Haley"

What if we wanted to find all active players that are not premium members? We would do the opposite of what we did with the IntersectWith operation by calling ExceptWith:

HashSet<string> activePlayers = new HashSet<string>() { "Harrison",
"Alex", "Haley"};
HashSet<string> premiumPlayers = new HashSet<string>() { "Haley",
"Basel" };

activePlayers.ExceptWith(premiumPlayers);
// activePlayers now stores "Harrison" and "Alex" but removed "Haley"
Notice that I'm using brand-new instances of the two example sets for each operation because the current set is modified after each operation is executed. If you keep using the same sets throughout, you will get different results.

Now that you've learned how to perform fast mathematical operations with HashSets, it's time to close our chapter and drive home what we have learned.

Summary

Congratulations, you're almost at the finish line! In this chapter, you learned about three new collection types, and how they can be used in different situations. Stacks are great if you want to access your collection elements in the reverse order that they were added, queues are your ticket if you want to access your elements in sequential order, and both are ideal for temporary storage. The important difference between these collection types and lists or arrays is how they can be accessed with popping and peeking operations. Lastly, you learned about the almighty HashSet and its performance-based mathematical set operations. In situations where you need to work with unique values and perform additions, comparisons, or subtractions on large collections, these are key.

In the next chapter, you'll be taken a little deeper into the intermediate world of C# with delegates, generics, and more as you approach the end of this book. Even after all you've learned, the last page is still just the beginning of another journey.

Pop quiz  intermediate collections

  1. Which collection type stores its elements using the LIFO model?
  2. Which method lets you query the next element in a stack without removing it?
  3. Can stacks and queues store null values?
  4. How would you subtract one HashSet from another?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.251.37