Author: Joe McMahon

  • leetcode day 29: 01 matrix

    Transform an arbitrarily-shaped rectangular matrix of zeroes and ones into a map of distance from the nearest zero, using a NSEW neighborhood.

    My first impulse was, “oh, find all the zeroes, and then work out from each one to fix the ones”, followed by “that sounds hard to do, I’ll try the recurrence relation and caching”. The recurrence relation and caching simply does not work because the problem space is too big. Even with caching the intermediate results I still ran out of memory because the neighborhood processing recursed 4x for every cell.

    So I went back to the original thought. I didn’t get much of anywhere until I got the hint that what I needed to do was mark all the 1’s as 999999 (an impossible value, marking them as “not done”). After that, I pulled up my memories of linked list processing and it was actually kind of easy! We start up by creating a QueueItem struct:

    type QueueItem struct {
        i int,
        j int,
        next *QueueItem
    }
    

    Now we need to find all the zeros and add them to the queue, and mark all the ones as “not done yet”. In hindsight, I should have made the queue operations at least a separate sub — mostly because there was a test case with a single zero, which messed up my enqueue/dequeue logic! — but they’re inline:

        m := len(mat)
        n := len(mat[0])
    
        for i := 0; i < m; i++ {
            for j := 0; j < n; j++ {
                if mat[i][j] == 0 {
                    item := &queueItem{
                        i: i,
                        j: j,
                        next: queue,
                    }
                    if queue == nil {
                        // tail stays here while we add to the head
                        tail = item
                    }
                    queue = item
                } else {
                    mat[i][j] = 9999999
                }
            }
        }

    Pretty straightforward — now we have all the zeroes on the queue, and can work our way out from each one. The 999999 is a value guaranteed greater than the largest possible real value in the map. Now all we have to do is process the neighborhoods around each zero; if we find a cell we need to update, we know from the definition of the problem that these are one away from the cell we took off the queue, so for the zero cells, these will all be ones. We set the value in the matrix, and then we add the cell we just processed to the tail of the queue. This way we process all the zeroes, then all the ones, and so on until we’ve processed every cell that needs it. When that finishes, we’re done and can return the updated matrix.

        neighborhood := [][]int{ {1,0}, {-1,0}, {0, -1}, {0, 1}}
    
        for queue != nil {
            // next cell in priority popped off the quque
            cell := queue
            queue = queue.next
            for _, dir := range neighborhood {
                // Figure out the next cell in the neighborhood of the one we
                // pulled off the queue.
                offsetI, offsetJ := cell.i + dir[0], cell.j + dir[1]
    
                // If we're in bounds...
                if offsetI >= 0 && offsetI < m && offsetJ >= 0 && offsetJ < n {
                    // ...and the cell we're looking at has a value > the current
                    // cell + 1, update it to the value of the current cell + 1
                    // and queue it. Note that neighbors closer to a zero or 
                    // zeroes themselves will be fail the test and will be
                    // ignored; they're already done.
                    if mat[offsetI][offsetJ] > mat[cell.i][cell.j] + 1 {
                        newCell := &queueItem{
                            i: offsetI,
                            j: offsetJ,
                        }
                        // This is the special case that bit me: it's possible
                        // that the queue emptied. Reset the head and tail 
                        // pointers to the cell we're adding.
                        if queue == nil {
                            queue = newCell
                            tail = newCell
                        } else {
                            // Didn't empty, so add to the tail. This guarantees
                            // that we process all items closer to a zero first.
                            tail.next = newCell 
                            tail = tail.next
                        }
                        // This updates the cell to the right distance and
                        // removes the "unprocessed" marker.                    
                        mat[offsetI][offsetJ] = mat[cell.i][cell.j] + 1
                    }
                }
            }
        }
    
        // when we arrive here there are no more queue items to process,
        // we've marked all the cells and are done.
        return mat

    So that’s it! The priority queue is stupid easy to do after the other list exercises, and I should have trusted my initial idea and tried it first. The problem I got at Bloomberg was actually a variation on this, and I could solve it fairly fast now.

    I will have to figure out dynamic programming at some point, but this problem didn’t need it.

  • leetcode day 29: insert interval big fail

    I chose completely the wrong way to solve this problem.

    Instead of eliminating all the elements that could not contain the inserted interval, I attempted to simultaneously inspect each interval while retaining the only interval and composing a new one. This led to deeply nested conditionals, and numerous flags attempting to track if I’d finished composing a new interval, had I inserted it, and so on.

    This problem is really about figuring out the elements that cannot contribute to the merged interval, and skipping over them as fast as possible. So we have two assertions:

    • The element strictly occurs before the new interval (end of the interval is < start of new interval)
    • The element strictly occurs after the new interval (beginning of the interval is > end of the new interval)

    With those in mind, the only thing we really have to do is collapse any intervals that start on or after the start of the new interval and end before or at the end of it. To collapse those intervals with the new interval, we simply have to keep taking the minimum of the start of the old or new interval as the start of the inserted interval, and the max of the end of the old or new interval as the end of the new one to stretch the new interval lower and higher as needed to fit.

    Once the new stretched interval is computed (remember, because we continue collapsing until the next interval (if there is one) is strictly after the new interval, which is will always be if the end of the stretched interval is < the start of the next one), we can simply insert the stretched interval!

    The finish appends the rest of the remaining items to the output, and then returns it.

    func insert(intervals [][]int, newInterval []int) [][]int {
        // Interval belongs somewhere in the list.
        // Look for insert point for new interval
        output := [][]int{}
        
        // Assertion: if interval[i][1] < newInterval[0], this interval 
        //            occurs before the new interval.
        // I is NOT in the for so its value hangs around after the loop ends.
        i := 0
        for ; i < len(intervals) && intervals[i][1] < newInterval[0]; i++ {
            output = append(output, intervals[i])
        }
    
        // At this point, the last interval in output is before or at the 
        // start of newinterval. collapse intervals until we find one that 
        // starts before thend of newinterval. i still points to the last
        // interval shifted.
        for ; i < len(intervals) && intervals[i][0] <= newInterval[1]; i++ {
            // adjust the start of the new interval to the earliest point
            if intervals[i][0] < newInterval[0] {
                newInterval[0] = intervals[i][0]
            }
            if newInterval[1] < intervals[i][1] {
                newInterval[1] = intervals[i][1]
            }
        }
    
        // add the stretched interval
        output = append(output, newInterval)
    
        // Copy anything left.
        for ; i  < len(intervals); i++ {
            output = append(output, intervals[i])
        }
        return output
    }

    I checked the solutions and found that there’s a function in the sort package named sort.Search that lets you construct an anonymous boolean function that is run over a sorted array to find the first item that meets the criterion specified by the function. So we can eliminate all of the explicit loops and write it like this:

    func insert(intervals [][]int, newInterval []int) [][]int {
        n := len(intervals)
    
        // Find first interval starting strictly before the new one
        // so we can extract it with a slice. i-1 will be the first one
        // that starts at a value <= the start of the new interval,
        i := sort.Search(n, func(i int) bool {
           return intervals[i][0] > newInterval[0] 
        })
    
        // Find all intervals ending strictly after this one, same thing.
        // j will be the first one definitely ending after the end of the
        // new interval.
        j := sort.Search(n, func(j int) bool {
           return intervals[j][1] > newInterval[1]
        })
    
        // Collapse intervals. If we're inside the array, and
        // the LAST interval with a start <= the start of the
        // new interval has a start that is <= the end of that
        // interval, they overlap. Stretch the bottom of the new
        // interval down and delete the overlapped interval.
        if i-1 >= 0 && newInterval[0] <= intervals[i-1][1] {
            newInterval[0] = intervals[i-1][0]
            i--
        }
    
        // If we're inside the array, and the FIRST interval with
        // an end AFTER the end of the new interval has a start <=
        // the end of the new interval, then they overlap. Stretch the
        // top of the new interval up and discard the overlapped interval.
        if j < n && newInterval[1] >= intervals[j][0] {
            newInterval[1] = intervals[j][1]
            j++
        }
    
        return append(
            // the slice up to the last interval strictly ending before
            // the start of the new interval
            intervals[:i],            
            append(
                // the new, possibly stretched interval
                [][]int{newInterval}, 
                // the slice from the first interval strictly starting
                // after the end of the new interval
                intervals[j:]...)...)
    }

    Internally, sort.Search does a binary search for the criterion function, so it’s very efficient. (And I should go back to the binary search questions and see if I can solve them more easily with sort.Search!) However, thinking through the conditions is a bit of a beast. Unless you have very large arrays to work on, the triple for loop may be easier to code on the spot and get right. Given the major improvement in speed, it’s worth practicing with!

    The sort.Search solution is very good: 94th percentile on speed, 92nd percentile on memory. Not surprising, as we’ve swapped the linear iteration over the intervals to a binary search, and we only compose the output array at the end with two appends.

    Interestingly, the linear recomposition three-loop version is quite fast too! 95th percentile, though the memory usage is only about the 50th percentile. I’d say stick with that solution unless there’s a definite case for the harder-to-code one.

    Lessons for me on this first “medium” problem, that I didn’t finish at all quickly? Spend more time thinking about how to eliminate data from the process. What is the fastest way to do that?

    My “sort it into place and track everything” solution was too complicated by far. If the logic to iterate over an array starts getting deeply nested, you are doing it wrong.

  • leetcode day 29: max sub array / Kadane’s algorithm

    First medium problem, and another IYKNK solution.

    Given an array of numbers, search it for the subarray that has the maximum sum, and return the sum.

    leetcode rule of thumb: any problem that has a solution with someone’s name on it is not an obvious solution. In this case, it’s Kadane’s method, which is simple but not intuitive enough that one would immediately stumble on it while trying to solve the problem.

    A naive solution brute-force sums the subarrays, looking for the best answer; this is O(N^2). Kadane’s algorithm is O(n), which is pretty impressive, and hearkens back to the tree diameter solution: while working on the subproblems, you maintain a global maximum and compare further work to it.

    func maxSubArray(nums []int) int {
        localMax := 0
        globalMax := -99999
        for i := 0; i < len(nums); i++ {
            if nums[i] > nums[i] + localMax {
                localMax = nums[i]
            } else {
                localMax = nums[i] + localMax
            }
            if localMax > globalMax {
                globalMax = localMax
            }
        }
        return globalMax
    }

    We start with a guaranteed-to-be-wrong answer for the global maximum, then start at the beginning of the array. If the next number along is > than the local maximum sum so far, then it’s the new global maximum. Otherwise, add it to the local maximum. If the local maximum is now > the global one, we’ve found a new global max subarray sum, and we save it.

    My original version came in disturbingly low in the runtime list, like 5%. I looked at it and said, “this is too simple for me to have gotten wrong, what’s the deal?”, and then I realized that this is another hyperoptimization hack probably unique to Go: Go has no builtin max() function, so I had coded one, adding another function call to each iteration.

    Since the implementation is so minimal as it is, that function call suddenly becomes very significant. Just swapping it over to an if-else immediately put me at 100%.

    Which goes to show you that the runtime as a percentage of the “best” runtime is, at best, kind of bullshit. However, I learned another trick to game leetcode, so I suppose that’s something.

  • leetcode day 28: not finishing this month, and tree diameter

    First: other projects popped their heads in: setting up a friend’s Etsy store, an announcement from Apple that I had 30 days to update an app before it was pulled, and the holidays. So I really haven’t touched the challenge since the 16th, or about ten days ago, and finishing all 75 problems in a month was already quite the challenge.

    The store is up and has had some orders already, and the app has been brought forward from Swift 3 to Swift 5, and updated to Xcode 15 (which ChatGPT cheerily insists doesn’t exist unless you pay money for its dubious advice at the ChatGPT 4 level). So let’s get back into the challenges.

    Another tree problem; this one requires us to find the longest leaf-to-leaf path in the tree, not necessarily going through the root. I struggled with this one a while, but did not hit on the need to have the current largest diameter outside of crawl; I eventually gave up and Googled a solution, which passed a pointer to the diameter into the crawler, so we always were looking at the absolute maximum across the whole tree.

    /**
     * Definition for a binary tree node.
     * type TreeNode struct {
     *     Val int
     *     Left *TreeNode
     *     Right *TreeNode
     * }
     */
    
    func diameterOfBinaryTree(root *TreeNode) int {
        diameter := 0;
        _ = crawl(root, &diameter);
        return diameter;
    };
    
    func crawl(root *TreeNode, diameter *int) int {
            if root == nil {
                return 0
            }
            leftSubtreeDepth := crawl(root.Left, diameter)
            rightSubtreeDepth := crawl(root.Right, diameter)
            *diameter = max(*diameter, leftSubtreeDepth + rightSubtreeDepth)
            return max(leftSubtreeDepth, rightSubtreeDepth) + 1
    }
    
    func max(x, y int) int {
        if x > y {
            return x
        }
        return y
    }

    This was 100% on speed, and 20% on memory.

    I was close; I had the nil root case, and I’d figured out I needed the depths of the trees, but I didn’t put together that I’d need to cache the diameter completely outside the crawler to accurately find the maximum – if you just pass it back recursively, a larger value can end up discarded.

  • leetcode day 16: evaluation

    So let’s take a breath; we’re just past the midpoint, and I don’t think I’ll finish the whole set of exercises this month, even though I’ve ripped through 11 today.

    I do feel like I’m learning things; some of them are generally useful, and others are “okay, you could do that if you really had to squeeze every microsecond out”.

    • The old Perl rule of thumb “if someone’s asking you to count things, start with a hash” has generally paid off well. There are a lot of tricky choices in this, but the following rules of thumb seem to be true:
      • If you are coding a for loop, putting the condition in the for seems to run fastest. It’s definitely preferable to a bare for{... with a conditional break inside.
      • Using range will often give you better CPU time at the expense of more memory.
      • Making two extra function calls on the nil subtrees of a leaf to a function that immediately drops out on the null case is better than having a leaf case inline.
    • I need to brush up on my tree-crawling algorithms. I know them well enough to code them and not break things, but the upcoming “diameter of a binary tree” problem is going to be one I have trouble with, similar to the trouble I had with lowest common ancestor and balanced tree: I simply am not comfortable enough thinking about trees to see the solutions easily; sometimes I’ve having trouble getting the problem statement clear enough to solve the problem.
    • Also need more practice with binary searches. I struggled a lot with the first binary search, but did a lot better with the second one in minimum bad version.
    • I have learned some interesting new things:
      • The Euler tour for transforming the lowest common ancestor problem into a maximum-in-range problem.
      • The tortoise-and-hare-pointer algorithm for detecting loops in a list turned out to be applicable to finding the middle of a linear list.
      • If a Go problem is mostly linear searching and adding and removing stuff from the front of a list, using a real linked list will often be significantly faster, though more memory-intensive.
      • Dipping a toe into dynamic programming. I definitely need to spend more time with this, as I definitely got a 2-D dynamic programming question at Bloomberg and wasn’t able to solve it, even in Perl.
      • Moore’s candidate algorithm for the majority element. Ridiculously simple…once you’ve seen it.
      • If you just need a thing to indicate that something is there in a map, a map[type]struct{} is much, much faster than any other type.
      • If your problem space is small enough, using an array instead of a hash will get you big speedups and save a ton of memory while still letting you count things by a key; it’s just that the key is now an integer (e.g., anagrams, if the letters are guaranteed to only be ASCII a-z).
      • Converting a byte array to a string is way faster than progressively building up a string by concatenation. If you can precalculate your maximum space requirement for the byte array, you can make it even faster by preallocating with make(), which lets you dynamically choose the initial size for the array.
      • string() on a slice is just as fast as string() on a full array.
      • It’s actually pretty easy to work with byte arrays, and they let you nicely mix numeric and character values for characters. (Though you should limit this to ASCII.)
    • Things I’m pleased about
      • I had less trouble than I expected with 2D arrays (flood fill). Getting the basic stuff right was the hard part, and after that it was straightforward.
      • I’ve gotten the hang (mostly) of passing pointers into Go functions, allowing me to do things like build persistent caches recursively.
      • I had my very own insight on the middle-of-a-linked-list problem that solved it with a fast and slow pointer.
      • I’ve used closures as iterators twice, and both times they significantly simplified the code.
      • I’m feeling better and better about my skill with Go. I’m definitely a very solid intermediate programmer, and I think I will be able to say I’m an advanced one after getting through all these problems. I’m much more comfortable with pointers in Go that I was when I started. The syntax is still a little gnarly.

    I have more stuff to do for Shymala’s art business tomorrow, but I hope to get back to things in a few days. I’ve finished week one and most of week two; I’m two exercise short and two days behind, but there were two weeks of pretty much continuous work on the business in there, so maybe I will finish it off this month. We’ll see.

    At any rate, I think I’ll feel a lot less anxious doing coding interviews once I’ve gone through this, and that was the point of this project.

  • leetcode day 16: linked list middle

    Given a linked list, find the middle node. For an odd number of nodes, we want node [n/2]+1; for an even number, we want node [n/2].

    The brute force method would run the length of the list, count up the number of nodes, and then start over from the beginning, counting off the appropriate number.

    However, if we think about what the pointers do (one moves all the way, one moves halfway), we can do both at the same time, and use the “I ran off the end” condition to know when to check the pointer trying to get to halfway.

    To do this, we run a fast pointer and a slow one, just like the loop detector tortoise and hare (and I’m patting myself on the back for having that insight), but in this case we use the fact that the lagging pointer moves exactly half as fast to ensure that it’s only gone halfway when the fast pointer runs off the end.

    I had a little trouble getting the conditions right, but the final version that hits the right spots is this one:

    /**
     * Definition for singly-linked list.
     * type ListNode struct {
     *     Val int
     *     Next *ListNode
     * }
     */
    func middleNode(head *ListNode) *ListNode {
        // tortoise and hare solution
        if head == nil {
            return nil
        }
    
        p := head
        q := head
        // we're not at the end, and there's at least one node past
        // this one, so q.Next.Next to jump forward 2 is valid.
        for q != nil && q.Next != nil {
            q = q.Next.Next
            // p moves forward one so that it will have moved half
            // as far. When q runs off the end, p will be at the halfway
            // point.
            p = p.Next
        }
        return p
    }

    This is clever in that it comes up with a nice way to combine the two actions, but not “clever” in the sense of “how does this even work”. This feels like a good strong choice rather than a “look how clever I am with character values” fragile one.

  • leetcode day 16: contains duplicate

    Given an array of integers, is there at least one duplicate in it?

    Again we’re counting things, but since we’re really caring about existence, the condition can be wrapped into the count loop:

    func containsDuplicate(nums []int) bool {
        counts := map[int]int{}
        for i:=0; i < len(nums); i++ {
            if _, ok := counts[nums[i]]; ok {
                return true
            }
            counts[nums[i]]++
        }
        return false
    }

    We look to see if we’ve counted this at least once, and if we have, there’s no more work to do. If we never count something twice, then the answer is false, and we’ve only done one pass at the most over the array.

    Speed 65%, memory 5%.

    func containsDuplicate(nums []int) bool {
        counts := make(map[int]struct{})
        for _,i := range(nums) {
            if _, ok := counts[i]; ok {
                return true
            }
            counts[i] = struct{}{}
        }
        return false
    }

    Better on memory because it uses an empty struct as the sentinel value instead of an int, and much faster: 94% CPU, 37% memory. Definitely going to put this in my bag of tricks.

  • leetcode day 16: max depth of binary tree

    After other problems related to heights, this is an easy one. Find the maximum distance from the root to a leaf in a binary tree.

    My first cut had an extra check for the null-left, null-right node, but I realized that the extra call with a nil pointer was faster than an extra conditional in the main path.

    **
     * Definition for a binary tree node.
     * type TreeNode struct {
     *     Val int
     *     Left *TreeNode
     *     Right *TreeNode
     * }
     */
    func maxDepth(root *TreeNode) int {
        if root == nil {
            return 0
        }
        return 1 + max(maxDepth(root.Left), maxDepth(root.Right))
    }
    
    func max(a, b int) int {
        if a >= b {
            return a
        }
        return b
    }

    100% on speed, 26% on memory. Finished about as fast as I could type it.

  • leetcode day 16: add two binary strings

    You are given two strings, possibly of different lengths, and you are to add them as if they were binary numbers, returning the result as a string.

    My first cut was to use an iterator function to scan over the strings, delivering the digits in the order I want them, then summing them via a switch statement. Getting the carry right was the only real issue.

    func addBinary(a string, b string) string {
        if a == "0" {
            return b
        } else if b == "0" {
            return a
        } else {
            iterA := nextDigit(a)
            iterB := nextDigit(b)
            max := len(b)
            if len(a) > max {
                max = len(a)
            }
            carry := 0
            sum := ""
            for i:=0; i < max; i ++ {
                x := iterA()
                y := iterB()
                switch x+y+carry {
                    case 0:
                    sum = "0" + sum
                    case 1:
                    sum = "1" + sum
                    carry = 0
                    case 2:
                    sum = "0" + sum
                    carry = 1
                    case 3:
                    sum = "1" + sum
                    carry = 1
                }
            }
            if carry != 0 {
                sum = "1" + sum
            }
            return sum
        }
    }
    
    func nextDigit(s string) func() int {
        digits := []byte(s)
        i := len(digits)-1
        var one byte = '1'
        return func() int {
            if i >= 0 {
                j := digits[i]
                fmt.Println(j)
                i--
                if j == one {
                    return 1
                } else {
                    return 0
                }
            } else {
                return 0
            }
        }
    }

    This is quite slow: 5th percentile in speed, 19th percentile in memory. Let’s look at speed hacks. The call to the closure for every digit is probably hurting both of those, so let’s inline the logic and Cheaty McCheatface the values so we’re working with ints as much as possible:

    unc addBinary(a string, b string) string {
        if a == "0" {
            return b
        } else if b == "0" {
            return a
        } else {
            ascan := len(a) - 1
            bscan := len(b) - 1
            carry := 0
            sum := ""
            for ascan >= 0  || bscan >=  0 {
                var aVal, bVal int
                if ascan < 0 {
                    aVal = 48
                } else {
                    aVal = int(a[ascan])
                    ascan--
                }
                if bscan < 0 {
                    bVal = 48
                } else {
                    bVal = int(b[bscan])
                    bscan--
                }
                switch aVal+bVal+carry {
                    case 48+48+0:
                    sum = "0" + sum
                    case 48+49+0:
                   sum = "1" + sum
                    carry = 0
                    case 49+49+0:
                    sum = "0" + sum
                    carry = 1
                    case 49+49+1:
                    sum = "1" + sum
                    carry = 1
                }
            }
            if carry != 0 {
                sum = "1" + sum
            }
            return sum
        }
    }
    
    func nextDigit(s string) func() int {
        digits := []byte(s)
        i := len(digits)-1
        var one byte = '1'
        return func() int {
            if i >= 0 {
                j := digits[i]
                fmt.Println(j)
                i--
                if j == one {
                    return 1
                } else {
                    return 0
                }
            } else {
                return 0
            }
        }
    }

    Better. 100% on speed, but 18% on memory, no doubt all those string allocations. Let’s append and then reverse the output instead.

    func addBinary(a string, b string) string {
        if a == "0" {
            return b
        } else if b == "0" {
            return a
        } else {
            ascan := len(a) - 1
            bscan := len(b) - 1
            max := ascan
            if bscan > ascan {
                max = bscan
            }
            // ensure we only allocate once
            byteString := make([]byte, max+2)
            optr := max+1
            carry := 0
    
            for ascan >= 0  || bscan >=  0 {
                var aVal, bVal int
                if ascan < 0 {
                    aVal = 48
                } else {
                    aVal = int(a[ascan])
                    ascan--
                }
                if bscan < 0 {
                    bVal = 48
                } else {
                    bVal = int(b[bscan])
                    bscan--
                }
                switch aVal+bVal+carry {
                    case 48+48+0:
                    //sum = "0" + sum
                    byteString[optr] = 48
                    optr--
                    case 48+49+0:
                    //sum = "1" + sum
                    byteString[optr] = 49
                    optr--
                    carry = 0
                    case 49+49+0:
                    //sum = "0" + sum
                    byteString[optr] = 48
                    optr--
                    carry = 1
                    case 49+49+1:
                    //sum = "1" + sum
                    byteString[optr] = 49
                    optr--
                    carry = 1
                }
            }
            if carry != 0 {
                byteString[optr] = 49
            } else {
                optr++
            }
    
            sum := string(byteString[optr:])
            return sum
        }
    }

    Significantly harder to get right (remembering to fix all of the magic numbers is a particular pain) but now memory usage is at the 78th percentile, which is a huge boost. I’d call that a winner, modulo the dependencies we’ve added to make the memory usage smaller.

  • leetcode day 16: majority element

    Given an array of n integers, find the majority element (the element whose count is >= [ n / 2]).

    Count means map.

    func majorityElement(nums []int) int {
        // Count means map.
        counts := map[int]int{}
        for _,v := range(nums) {
            counts[v]++
        }
        targetCount := len(nums) / 2
        currentMaxCount := 0
        target := -9999999999
        for k,v := range(counts) {
            if v > targetCount && v > currentMaxCount {
                currentMaxCount = v
                target = k
            }
        }
        return target
    }

    Scored top 90% on time, top 40% on memory. But if you know the trick (Moore’s candidate algorithm)…

    func majorityElement(nums []int) int {
        // "If you know the trick"...Moore's voting algorithm
        count := 0
        var target int
        for i := 0; i < len(nums); i++ {
            if count == 0 {
                target = nums[i]
                count++
            } else if nums[i] == target {
                count++
            } else {
                count--
            }
        }
        return target
    }

    Gets us to 94% time, 71% memory. Using range() causes Go to use more memory (drops to 19%!).

    The “best” algorithm is not that much better, so I’ll call my solution good.