[)roi(]
Executive Member
- Joined
- Apr 15, 2005
- Messages
- 6,282
There's been a fair amount of opinions shared over the last period about the merits of data structure and algorithm questions in a technical interview; specifically the type of questions that require you to step up to a whiteboard and flesh out in front of an audience.
This is not another one of those; rather in this thread I'm going to turn it into a question vs. answer session on:
So to start, I'm going to open with a question that a few colleagues approached me on; more specifically on how I'd go about solving it & it just happens to be also one that's come up fairly frequently in discussion on twitter, namely:
Singly Linked List (re Wikipedia)
https://en.wikipedia.org/wiki/Linked_list
In short; it's a way to sequentially store data, albeit not a very efficient one; aside from being able to quickly prepend another node.
As for the solutions; I'm going to focus on providing answers in Swift, and I intend to leave the other languages for anyone else willing to take up the challenge.
First off let's define the type. If we look at the image of the Singly Linked List (SLL), we should be able to see that it's repetitive: 1st node linked to 2nd node, etc. Another name for repetitive is recursive. So my first choice will be to use a recursive data type. In Swift the first one that comes to mind is enums or more specifically indirection with enums. Swift enums are very different from those found in traditional C style languages; Swift enum implementation is far closer to Tagged Unions (also called Discriminated Unions).
Well onto the code... Here's the recursive enum type for Singly Linked Lists(SLL):
Basically we have two possibilities with a SLL node; it can either be None (the end of the list), or it can be a Node (which contains a value, and a link to the next node). The indirect keyword on the node case informs the compiler that we are creating a recursive link from this case.
Now let's look at how we would use this SLL type to initialise a list with the letters of the word REVERSE.
That's kind of busy to look at, so let's add some code to print a more friendlier version. So going to back to the original definition for the enum, we are going to change the default way that structure is printed (this is equivalent of the toString in e.g. C#)
Basically we've added a protocol (Interface) compliance (CustomStringConvertible); this informs Swift that we intend to override it's default output format. Next we are required to redefine the description property. The code includes a recursive function called iterate that iterates through the nodes of the SLL, until it reaches None, at each step we append the result of the Node value to result, and then pass this new result to the next iteration.
Here's what the output from this looks like:
Next let's get onto the main question; that of reversing the SLL, here's the code, added as an extension to the our SLL type:
Again I've used a recursive function that starts with the 1st if statement i.e. by default the parameters lhs & rhs are set to .None, so we start with self (the instance of our SLL), and .None for the value. Basically when we reverse the list we are starting from the bottom so the 1st element (at the bottom), will have a value of None to which we'll linked the values in reverse order. Let's call that function and see it's output.
Anyway that's it, problem solved.
Please post your common whiteboard programming questions, and hopefully someone will provide a solution (in the language of their choosing); failing which I'll follow up with a Swift solution (after ~1 week, or sooner if you're hasty for an answer). Naturally I encourage you to try and solve these yourself.
Note:
These are not to be brain teaser questions but rather the typical questions that come up in interviews.
/Edit After @cguy's comments, I have changed the function name from reverse() to reversed() re Swift's naming convention / API guidelines for differentiating between in place (mutating: reverse) operation vs. new list (reversed: non-mutating).
This solution as @cguy surmised is non-mutating i.e. a new list created (old memory is freed, new memory is allocated), whereas mutating would imply the use of the existing memory allocation. However Swift enums are value types, meaning they are specific designed to not permit any mutation i.e. Functional Programming Paradigm. Should you wish to do an in-place reverse of the SLL, you would have to redesign this using a reference type like classes.
Why does this matter?
The process of allocating, freeing and reallocating memory comes at a cost, this cost is significantly reduced with an in-place reversal. So if resource management is key, or your SLL is significantly large, you may want to opt for references types to perform an in-place reversal of the reference pointers.
Swift btw tends to provide both options for the majority of the collection types, for example:
So why would you ever use the non mutating option?
Parallel processing is a complex topic that many either never master or get significantly wrong.
Value types (non-mutating) makes it easy; because every thread and/or processing node has a unique dataset that is unaffected by what is happening on the other threads / nodes. For example: massively parallel sequence processing.
As for the problems encountered with mutable data types and threading, I suggest you do some research on the broader topic of "data races":
This is not another one of those; rather in this thread I'm going to turn it into a question vs. answer session on:
- The most common and/or challenging questions being asked in programming related interviews, and then follow this up with a solution.
- Naturally it'd be great if this isn't one sided; solutions to the problems should be open to anyone willing to try.
So to start, I'm going to open with a question that a few colleagues approached me on; more specifically on how I'd go about solving it & it just happens to be also one that's come up fairly frequently in discussion on twitter, namely:
- Singly Linked Lists, or more specifically reversing one of these.
Singly Linked List (re Wikipedia)
- A linked list is a linear collection of data elements, called nodes, each pointing to the next node by means of a pointer.
- It's a data structure consisting of a group of nodes which together represent a sequence.
https://en.wikipedia.org/wiki/Linked_list
In short; it's a way to sequentially store data, albeit not a very efficient one; aside from being able to quickly prepend another node.
As for the solutions; I'm going to focus on providing answers in Swift, and I intend to leave the other languages for anyone else willing to take up the challenge.
First off let's define the type. If we look at the image of the Singly Linked List (SLL), we should be able to see that it's repetitive: 1st node linked to 2nd node, etc. Another name for repetitive is recursive. So my first choice will be to use a recursive data type. In Swift the first one that comes to mind is enums or more specifically indirection with enums. Swift enums are very different from those found in traditional C style languages; Swift enum implementation is far closer to Tagged Unions (also called Discriminated Unions).
Well onto the code... Here's the recursive enum type for Singly Linked Lists(SLL):
Code:
enum SLL<T> {
case None
indirect case Node(T, SLL<T>)
}
Now let's look at how we would use this SLL type to initialise a list with the letters of the word REVERSE.
Code:
let list: SLL = .Node("R", .Node("E", .Node("V", .Node("E", .Node("R", .Node("S", .Node("E", .None)))))))
That's kind of busy to look at, so let's add some code to print a more friendlier version. So going to back to the original definition for the enum, we are going to change the default way that structure is printed (this is equivalent of the toString in e.g. C#)
Code:
enum SLL<T>: CustomStringConvertible {
case None
indirect case Node(T, SLL<T>)
// Recursive String Conversion Property
var description: String {
func iterate(_ lhs: SLL<T>, _ result: String) -> String {
var result = result
switch lhs {
case .None:
result += ".eoll."
case let .Node(l, r): result = "\(l) → " + iterate(r, result)
}
return result
}
return iterate(self, "")
}
}
Here's what the output from this looks like:
Code:
print(list) // R → E → V → E → R → S → E → .eoll.
Next let's get onto the main question; that of reversing the SLL, here's the code, added as an extension to the our SLL type:
Code:
extension SLL {
func reversed(_ lhs: SLL<T> = .None, _ rhs: SLL<T> = .None) -> SLL<T> {
if case .None = lhs, case .None = rhs {
return reverse(self, .None)
}
switch lhs {
case .None: return rhs
case let .Node(l, r): return reverse(r, .Node(l, rhs))
}
}
}
Code:
print(list.reversed()) // E → S → R → E → V → E → R → .eoll.
// equivalent to: Node("E", .Node("S", .Node("R", .Node("E", .Node("V", .Node("E", .Node("R", .None)))))))
Anyway that's it, problem solved.
Please post your common whiteboard programming questions, and hopefully someone will provide a solution (in the language of their choosing); failing which I'll follow up with a Swift solution (after ~1 week, or sooner if you're hasty for an answer). Naturally I encourage you to try and solve these yourself.
Note:
These are not to be brain teaser questions but rather the typical questions that come up in interviews.
/Edit After @cguy's comments, I have changed the function name from reverse() to reversed() re Swift's naming convention / API guidelines for differentiating between in place (mutating: reverse) operation vs. new list (reversed: non-mutating).
This solution as @cguy surmised is non-mutating i.e. a new list created (old memory is freed, new memory is allocated), whereas mutating would imply the use of the existing memory allocation. However Swift enums are value types, meaning they are specific designed to not permit any mutation i.e. Functional Programming Paradigm. Should you wish to do an in-place reverse of the SLL, you would have to redesign this using a reference type like classes.
Why does this matter?
The process of allocating, freeing and reallocating memory comes at a cost, this cost is significantly reduced with an in-place reversal. So if resource management is key, or your SLL is significantly large, you may want to opt for references types to perform an in-place reversal of the reference pointers.
Swift btw tends to provide both options for the majority of the collection types, for example:
- sorted() - is a non-mutating (new copy) sort.
- sort() - is a mutating (in-place) sort for memory / performance reasons.
So why would you ever use the non mutating option?
Parallel processing is a complex topic that many either never master or get significantly wrong.
Value types (non-mutating) makes it easy; because every thread and/or processing node has a unique dataset that is unaffected by what is happening on the other threads / nodes. For example: massively parallel sequence processing.
As for the problems encountered with mutable data types and threading, I suggest you do some research on the broader topic of "data races":
- https://en.wikipedia.org/wiki/Race_condition
- https://github.com/google/sanitizers/wiki/ThreadSanitizerPopularDataRaces (Google summary of some most "popular" data races)
Last edited: