Showing posts with label Programming. Show all posts
Showing posts with label Programming. Show all posts

Thursday, February 2, 2012

Bitmap Sort

In Jon Bentley's Programming Pearls book, the first Column introduces a sorting problem. As we learn more about the problem and clearly define it's constraints, the solution transitions from Merge Sort using Disk to an efficient Bitmap Sort.

This algorithm uses a bitmap (or a bit vector) to represent a finite set of distinct integers. For example, if we have an integer range 0-5, we can represent it using a 6-bit array, e.g.
[2,3,5] becomes 0 0 1 1 0 5
[1,3,4] becomes 0 1 0 1 1 0

In order to sort an array of integers, we first need to initialize a bit array of size corresponding to the range and fill it with zeroes (in Java, this is the default value). Then we go through the input array and set the corresponding bit in our bitmap to 1 for each input integer. Finally, we can scan through the bitmap and output the number for each bit that's set to 1. Since we're scanning the bitmap in order, we print all the original integers in a sorted order.

Implementation
This sounds like a very simple algorithm, that also runs in linear O(n) time! However, when I started to implement this algorithm in Java, a number of implementation details popped up and the faults of this algorithm also became apparent.

Most languages I know of won't let you to simply define a bit array -- the smallest primitive type is usually the 8-bit byte. For a range N, we will need to define an byte array of size N/8. And for each number i from input, we will set the i mod 8 'th bit of the floor(i / 8) 'th element of the bitmap. For example, if we had the range 0-15 (N=16) and the input integer was 13, we would set the 5th bit of the 1st element of the bitmap.

When we iterate through the bitmap, we go through each bit in each element and print out the number i * 8 + j for every j 'th bit set in the i 'th element.

This is my Java code:
      public int[] sortDistinctIntegers(int[] a, int min, int max){  
           int N = (max-min) / 8 + 1;  
           byte[] bitmap = new byte[N]; //initialized to 0  
             
           for(int i = 0; i < a.length; i++)  
                bitmap[a[i]/8] |= 1 << (a[i] % 8);  
             
           int k = 0;  
           for(int i = 0; i < N; i++){  
                for(int j = 0; j < 8; j++){  
                     if((bitmap[i] & (1 << j)) > 0){  
                          a[k] = i * 8 + j + min;  
                          k++;  
                     }  
                }  
           }  
             
           return a;  
      }  

In order to save space, I use the original array to "print" the sorted numbers, by keeping the counter k. Also notice that I've defined the range by the min and max variables, and I use the min variable as the offset when setting the bitmap and also when setting sorted integers. Note that this will also work for negative integers.

Shortcomings
This is where the shortcomings become apparent. First, we need to know the range in advance. If we don't, we cannot use this method for the 64-bit longs, since the range of the long type is larger than the largest allowable size for an array in Java (the maximum integer value). The maximum allowable range would be (2^32)-1*8 -- by having a byte array of maximum number of elements, each representing 8 numbers. Of course, we can increase the effective range by adding more dimensions.

Actually, the code above in its current form will not work for the maximum range between Integer.MIN_VALUE and Integer.MAX_VALUE because there will be an overflow in the (min-max) integer calculation. In order to fix that, we need to hack the expression so it evaluates the intermediate value as long and then converts back to integer after the division by 8 (this works because the division puts the range back into the integer land), so it will look like this:
 int N = (int)(((long)max-min) / 8 + 1);  
We will also need to do this in the bitmap location calculation, which will then look like this:
 bitmap[(int)(((long)a[i]-min)/8)] |= 1 << (int)(((long)a[i]-min) % 8);  

Second, even if we know the range, this method will be inefficient for sparse input (with very wide range and very small number of elements) -- and not just in space, since it takes a while to initialize and fill a 2 billion element byte array.

Performance Analysis
To evaluate the performance of this algorithm, I compare it to Java's built-in sort function. My testing code is as follows:
      public static void main(String[] args) {  
           BitmapSort bs = new BitmapSort();  
             
           Random gen = new Random();  
           Set<Integer> numbers = new HashSet<Integer>();  
           int input[];  
             
           for(int N = 10; N <= 2000000000l; N*=10){  
                System.out.print(N + ",");  
                  
                while(numbers.size() < N){  
                     numbers.add(gen.nextInt());  
                }  
                  
                input = new int[N];  
                int i = 0;  
                for(Integer num : numbers){  
                     input[i] = num;  
                     i++;  
                }  
                  
                int temp[] = input;  
                long t1 = System.currentTimeMillis();  
                Arrays.sort(temp);  
                System.out.print((System.currentTimeMillis() - t1) + ",");  
                  
                t1 = System.currentTimeMillis();  
                bs.sortDistinctIntegers(input, Integer.MIN_VALUE, Integer.MAX_VALUE);  
                System.out.println(System.currentTimeMillis() - t1);  
           }  
      }  

I use a HashSet to keep distinct integers generated from the Random class and then time Java's sort execution and my Bitmap Sort implementation.

The chart below shows my results:

Note that the code did not execute for input size larger than 10 million as the HashSet structure ate all the memory in the generation phase (even with 6GB allocation).



Java's sort performance is much faster for the small sizes, but starts to grow exponentially towards the millions zone. I have a feeling that Bitmap Sort might overtake Java's default sorting algorithm for larger input sizes (although, I'm not sure if Java switches algorithms at certain input size).

This experiment was of course for the worst case of Bitmap Sort -- with integers within the maximum range. However, if the range is much better defined (as would be the case in a lot of practical situations), the algorithm should perform much better.

Running the experiment with numbers generated in the range between 0 and N * 2 gives the follow results:



In this case, Bitmap Sort outperforms Java's system sort for all input sizes and looks like it scales better too!

Wednesday, October 5, 2011

Recursion IV: More Recursion Problems

This is a catch-up post, as I've been solving more recursion problems since the last post, but haven't had the chance to put them up on the blog. Since the last post, I started working on the problems from the book: Cracking The Coding Interview.

The book is written by Gayle Laakmann, an engineer at Google with a passion to train and mentor other people to become better computer scientists. He has plenty of experience interviewing at Google and other top companies, and in this book he shares his advice for most of the aspects of those sorts of interviews.

Major part of the book consists of interview questions, categorized by topic area, and the possible solutions. Recursion only comes in Chapter 8, but that's where I jumped first nonetheless, as this is my training focus right now.

Coin Change
The first problem I went on to solve involved calculating the number of ways N cents can be represented using quarters, dimes, nickels and pennies. This is my code:
      public static void getChange(int n, int[] coins, int sum){  
           if(sum == n)  
                System.out.println(coins[0] + "q," + coins[1] + "d," +   
                          coins[2] + "n," + coins[3] + "p");  
           else{  
                if(sum + 25 <= n){  
                     coins[0]++;  
                     getChange(n, coins, sum + 25);  
                     coins[0]--;  
                }  
                if(sum + 10 <= n){  
                     coins[1]++;  
                     getChange(n, coins, sum + 10);  
                     coins[1]--;  
                }  
                if(sum + 5 <= n){  
                     coins[2]++;  
                     getChange(n, coins, sum + 5);  
                     coins[2]--;  
                }  
                if(sum + 1 <= n){  
                     coins[3]++;  
                     getChange(n, coins, sum + 1);  
                     coins[3]--;  
                }  
           }  
      }  
The idea behind the code is relatively simple: build a recursion tree where each node branches off to 4 possibilities, while always making sure than proceeding to a certain branch will not take us over the given sum. Once we reach the desired sum, we are at a solution leaf, so print out the result.

Evaluation
One way I could improve the design of this code is by using some OO principles and wrap the Coins structure as an object and have the function return a list of such objects. I avoided that here for simplicity, as this is just proof of recursion concept.

There is a problem with this code in that it will return duplicate combinations of coins. The reason is because there is no differentiation between two pennies, two dimes, etc., so we can reach the same combination by taking different decisions at each recursion depth. For example, to represent 6 we can either select 6 pennies by going depth first on pennies branches, or we can select a nickel and a penny; however, selecting a penny and then a nickel will also be a possibility, which is impossible distinguish until we reach both of those leafs and notice the duplicate.

After reading the provided solution, I realized that I misread the question - it is asking for the number of ways, not the enumeration of all possibilities. The solution in the book also employs bottom-up recursion, as opposed to my top-down method.

It took me a while to understand the solution presented in the book. As we go deeper into the recursion tree, we solve the subproblem of making change for a smaller amount. So, when making change for 25, we look for ways to make change using 0 quarters, 0 dimes, 0 nickels first (i.e. using pennies). Then, we use 1 nickel, 2 nickels, etc., until we hit the sum limit. The base case of recursion is when we hit the lowest denominator - pennies, as we can always make up for remaining amount using pennies.

Balanced Parenthesis
The next problem I went on to solve was enumerating combinations of n-pairs of parenthesis. This one was fairly simple, and the my code closely matched the provided solution in the end:
      public List<String> generateParenthesis(int n){  
           List<String> list = new LinkedList<String>();  
           _generateParenthesis(0, 0, n, new String(), list);  
           return list;  
      }  
        
      private void _generateParenthesis(int open, int closed, int n, String s,   
                List<String> list){  
           if(closed == n)  
                list.add(s);  
           else{  
                if(open < n)  
                     _generateParenthesis(open + 1, closed, n, s + "(", list);  
                if(open > closed)  
                     _generateParenthesis(open, closed + 1, n, s + ")", list);  
           }  
      }  
We basically keep track of number of opened and closed parenthesis, opening parenthesis until we hit the provided limit and closing parenthesis while there are parenthesis to close (#open > #closed). Doing so recursively enumerates all possible combinations.

The only thing I noticed is that the book is missing one possibility for its provided example of 3 pairs: "(()())". The solution in the book also just prints out the solution at the base case, while I like to return the solution list as an object and use an "entry" method to recursion for a more user-friendly code design.

String Permutations
Finally, I worked on the interesting problem of string permutations. The problem definition is simple: given a string, output all possible permutations of its characters. I approached the solution this way:
      private static List<String> permuteString(String s){  
           List<String> list = new LinkedList<String>();  
           _permuteString(s, 0, list, new char[s.length()], new boolean[s.length()]);  
           return list;  
      }  
        
      private static void _permuteString(String s, int i, List<String> list, char[] p,   
                boolean[] used){  
           if(i == s.length()){  
                list.add(String.valueOf(p));  
           }  
           else{  
                for(int j = 0; j < s.length(); j++){  
                     if(used[j])  
                          continue;  
                     else{  
                          p[i] = s.charAt(j);  
                          used[j] = true;  
                          _permuteString(s, i+1, list, p, used);  
                          used[j] = false;  
                     }  
                }  
           }  
      }  
I approached this problem the same way as the number permutation problem. At each depth, I branch out to all possible characters from the string, but keep track of the characters I've already used (represented by the index at the original string and stored in the used[] array).


      public static List<String> permuteString(String s){  
           if(s.length() == 0){  
                return new LinkedList<String>(Arrays.asList(""));  
           }  
           else{  
                char first = s.charAt(0);  
                List<String> permutations = new LinkedList<String>();  
                for(String p : permuteString(s.substring(1)))  
                     combine(p, first, permutations);  
                return permutations;  
           }  
      }  
        
      private static void combine(String s, char c, List<String> combinations){  
           if(s.length() == 0)  
                combinations.add(String.valueOf(c));  
           else{  
                combinations.add(s + c);  
                combinations.add(c + s);  
                for(int i = 1; i < s.length(); i++)  
                     combinations.add(s.substring(0, i) + c + s.substring(i, s.length()));  
           }  
      }  
Here, at each deeper recursion level, we permute the substring of the original string, doing additional "merging" type operation as we go back up. E.g. when permuting "XYZ", we permute empty string first, then combine "Z" with the the empty string -> one way to do it: "Z", then we combine "Y" with permutation of "Z" -> two ways to do it: "YZ" or "ZY", then finally we combine "X" with permutations of "YZ" -> three ways to do it: "XYZ", "YXZ", "YZX", then three ways to do it with "ZY": "XZY", "ZXY" "ZYX", giving us all 6 combinations in the end.

This may seem like an intuitive and clever way to do it, but the top-down approach works better in this case. This is because even though the overall running time is bounded by O(n!), the combination part of top-down approach takes O(1) time, while doing the combination in bottom-up approach takes O(n).

I've ran the method for up to 10 characters, giving the following performance graph:


Note that the y-axis is logarithmic, and for the smaller input size, the top-down method is over an order of magnitude faster than the bottom-up approach. As we approach larger n, the asymptotic n! takes over, and the relative difference between the two method becomes marginal.

Friday, September 30, 2011

Recursion III: Divide and Conquer, and Mergesort

I now feel that it's time to tackle more advanced applications of recursion, especially those that are used in real-world algorithms - such as mergesort. At this point, I only vaguely remember how mergesort works, apart from the fact that it uses the recursive divide-and-conquer technique. Before finally diving into my algorithms book, I want to try to make sorting work on my own - using recursion - and then compare it to the actual algorithms presented in the book.

My Mergesort
The basic operation of any sort is the swap, where two values in the array get swapped, (hopefully) making progress to the correct sort order. So if we're using recursion, the base case would involve doing a swap. This would sort two values for us, and we can split the whole array into these small 2-element parts and sort each one individually at the deepest recursion level. Of course, that will not sort the whole array for us, but are we making progress for the global order?

Consider this example of 8-element array (sticking to power of 2 for simplicity): {5,7,8,4,6,5,3,2}. We can split it at the first level into {5,7,8,4} and {6,5,3,2}. Then further, e.g. in the left branch, to {5,7} and {8,4}. We can now sort these two parts, we get {5,7} and {4,8}. When we go up a level, the super-part {5,7,4,8} is still not sorted. However, since we know that the two halves are sorted, we only need to compare the respectively ordered cells between the two parts, i.e. the first element in first and second part, the second element in first and second part, etc. In our example, we compare 5 and 4, 7 and 8, swapping if necessary. This gives us {4,7,5,8}. Notice that the list is still not sorted. We still need to swap the inner 2 elements, giving us the final sorted list {4,5,7,8}. This is the merge part of the sort - we merged the sorted {5,7} and {4,8} into a sorted {4,5,7,8}.

Now can answer the question: was sorting the smallest sub-list necessary? If we did not sort {5,7} and {8,4} before merging the two lists, we merging on upper-level would not get us a sorted list. Starting from {5,7,8,4}, we compare 5 and 8, 7 and 4, giving us {5,4,8,7}. Comparing 4 and 8 doesn't change the order. The final list is not sorted, because 7 and 8 were not swapping at the deepest level.

Here is my implementation of this method:
      private void swap(Integer[] a, int i, int j){  
           if(a[j] < a[i]){  
                int temp = a[j];  
                a[j] = a[i];  
                a[i] = temp;  
           }  
      }  
        
      private void merge(Integer[] a, int i, int n){  
           if(n == 1)  
                swap(a, i, i+1);  
           else{  
                for (int k = 0; k < n; k++)  
                     swap(a, i+k, i+n+k);  
        
                int m = n-1;  
                merge(a, i+1, m);  
           }  
      }  
        
      private void mergeSort(Integer[] a, int i, int n){  
           if(n == 2){  
                swap(a, i, i+1);  
           }  
           else{  
                int m = n/2;  
                mergeSort(a, i, m);  
                mergeSort(a, i+m, m);  
                merge(a, i, m);  
           }  
      }  
The way I tested this code is by generating a random array of integers and then sorting the same array with the Java's provided sorted, comparing with my order in the end:
      public static void main(String[] args) {  
           MergeSort ms = new MergeSort();  
             
           int n = 1024;  
           Integer a[] = new Integer[n];  
           Random r = new Random();  
           for (int i = 0; i < n; i++) {  
                a[i] = r.nextInt(Integer.MAX_VALUE);  
           }  
             
           Integer b[] = a.clone();  
           Integer c[] = a.clone();  
             
           ms.mergeSort(b, 0, a.length);  
           Arrays.sort(c);  
             
           System.out.print(Arrays.equals(b, c));  
      }  
The code passes multiple runs, but there are several problems still:
1) The code does not work with array sizes other than powers of 2
2) There is a stack overflow exception for large input, e.g. 2^20

The second problem can be remedied by setting the stack memory size to 8MB: -Xss8192k. The first requires some extra thought. When splitting the array in mergeSort(), we will need to take the ceiling of n/2 to make sure we keep the n even. We also need to make sure j does not go out of bounds in the swap() function (for right-most nodes on the right branch). The code works correctly with those modifications:
      private void swap(Integer[] a, int i, int j){  
           if(j < a.length && a[j] < a[i]){  
                int temp = a[j];  
                a[j] = a[i];  
                a[i] = temp;  
           }  
      }  
        
      private void merge(Integer[] a, int i, int n){  
           if(n == 1)  
                swap(a, i, i+1);  
           else{  
                for (int k = 0; k < n; k++)  
                     swap(a, i+k, i+n+k);  
        
                int m = n-1;  
                merge(a, i+1, m);  
           }  
      }  
        
      private void mergeSort(Integer[] a, int i, int n){  
           if(n == 2){  
                swap(a, i, i+1);  
           }  
           else{  
                int m = (int)Math.ceil((double)n/2);  
                mergeSort(a, i, m);  
                mergeSort(a, i+m, m);  
                merge(a, i, m);  
           }  
      }  
The code takes a while to execute, even for a moderate sample size 30k. I have a feeling that there is some inefficiency, particularly in the merge method. For input size 1024 (with fixed Random seed), my method took almost 40 times longer than the Java's sort. Looking at the JProfiler results, we can see the problem:








n^2 running time of bubble sort.

Comparing Solutions
Now it's time to delve into the book and compare my solution with one provided there. I'm sure the actual mergesort algorithm will be much faster than mine. Let's see where exactly I went wrong.

Turns out that overall, the code structure and the idea I had was very close to the original algorithm. As I expected, the biggest problem was in the merge operation. Merging is done in O(n), with at most n/2 comparisons when merging two lists of overall size n. The lists are merged by continuously comparing the first element of each of two lists and building a new, sorted list (instead of doing all the swapping in-line). This obviously reduces the number of comparisons we need to do in order to merge two lists. Also, the number of required comparisons scales linearly, and not exponentially (as in my case).

Fixing My Solution
Let's try to fix my algorithm to run as efficiently as the original mergesort algorithm:
      private void merge(int[] a, int i, int m, int n){  
           int[] temp = new int[n];  
        
           int j = i; //start index of left part  
           int k = i+m; //start index of right part  
           for (int l = 0; l < temp.length; l++) {  
                if(j < i+m){  
                     if(k < i+n){  
                          if(a[j] <= a[k])  
                               temp[l] = a[j++]; //pick first from left  
                          else  
                               temp[l] = a[k++];  
                     }  
                     else  
                          temp[l] = a[j++]; //pick first from left  
                }  
                else  
                     temp[l] = a[k++]; //pick first from right  
           }  
             
           for (int l = 0; l < temp.length; l++)  
                a[i+l] = temp[l];  
      }  
        
      private void mergeSort(int[] a, int i, int n){  
           if(n == 1)  
                return;  
           else{  
                int m = n/2;  
                mergeSort(a, i, m);  
                mergeSort(a, i+m, n-m);  
                merge(a, i, m, n);  
           }  
      }  
First thing you might notice is that I'm now using primitive int type. I was curious if the Object wrapper would be significantly slower than the primitive type, and this page suggests using primitives in most situations. I double checked that primitive arrays are passed by reference (i.e. the reference to the array is passed by value, as Java is pass by reference for everything except non-array primitive types).

Evaluation
I've tested my solution against the Java built-in sort for input size 1 million and 10 million (with multiple Random attempts). My code ran correctly for all input, and took about 2.5 times longer than Java's sort (which must be using something like quicksort). This is still much much faster than my original version and consumes way less memory!

Tuesday, September 27, 2011

Recursion II: Permutations (or Let's Do Some Magix)

In the first post we looked at simple examples where an iterative solution seemed as natural - if not more - than its recursive counterpart. Now that we've mastered the simplest examples, we can start looking at problems where a recursive solution seems like a natural approach.

Magic Squares
One are of such problems is permutations, i.e. possible combinations of ordering certain items. I'm still following a great tutorial from here, so we will look at Magic Squares first. The problem of Magic Squares is to generate a square (2x2, 3x3, ... NxN) grid of numbers (1 to N^2) such that the sum in each row, column and diagonal is equal to the magical constant (N^3+N)/2, e.g. for 3x3 square we can only use numbers 1...9 and the sums should equal 15.

Before reading into the solution, we can try solving the problem ourselves. The first thing to worry about is representation of the problem in code. To me, the way that made most sense was to use a 1-dimensional array to represent the whole square - I figured that it will be easier to use recursion in 1-dimension.

Checking Candidate Solution
The first thing I did was writing a check function that would accept a representation of a candidate magic square solution and evaluate its magic:
      private boolean checkAnswer(int[] square){  
             
           int size = (int)Math.sqrt(square.length);  
           int constant = ((int)Math.pow(size, 3) + size) / 2;  
             
           //check row sums  
           for (int i = 0; i < square.length; i+=size) {  
                int sum = 0;  
                for (int j = i; j < i+size; j++) {  
                     sum += square[j];  
                }  
                if(sum != constant) return false;  
           }  
             
           //check column sums  
           for (int i = 0; i < size; i++) {  
                int sum = 0;  
                for (int j = i; j < square.length; j+=size) {  
                     sum += square[j];  
                }  
                if(sum != constant) return false;  
           }  
             
           //check first diagonal sum  
           int sum = 0;  
           for (int i = 0; i < square.length; i+=size+1)   
                sum += square[i];  
           if(sum != constant) return false;  
             
           //check second diagonal sum  
           sum = 0;  
           for (int i = size-1; i < square.length-1; i+=size-1)   
                sum += square[i];  
           if(sum != constant) return false;  
             
           //all tests passed  
           return true;  
      }  
We first calculate the size of the square in 1-dimension, i.e. its row/column/diagonal size by taking the square root of the total size (length) of the input array. It would be wise to make a check here to make sure the input array is indeed square, but I omitted it here for simplicity. The magic constant is calculated using the formula from the wikipedia page. Note that the magic does not work for square size 2x2, it is forever cursed...

Next, we calculate the appropriate sums. For row sums, we sum up size segments in sequential order, e.g. for 3x3 it would check sums 0+1+2, 3+4+5, 6+7+8. For column sums, we need to jump in size chunks, but starting from different point each time (for each column), e.g. for 3x3 we sum 0+3+6, 1+4+7, 2+5+8. Finally for the diagonal, we just need to start at 0 and jump size+1 at a time to land on the diagonal, e.g. for 3x3 it's 0+4+8. And for the second diagonal, we start at the edge but jump size-1 at a time, e.g. 2+4+6 for the 3x3 square.

I tested the code with the 3x3 example from the wikipedia page and made sure it returns false when some numbers are changed.

Baseline Recursive Solution
Now it's time to find the recursive solution. One way is to look at the search space as a tree, with the choice of 1..n^2 numbers as branches and the total number of cells to fill as the depth of the tree. This tree represents all possible candidate solutions to the problem. A tree is also a natural representation for recursion, so it shouldn't be too hard to implement from here. Here is my solution:
      private int[] solveSquare(int[] square, int i){  
           if(i == square.length){  
                if(checkAnswer(square))  
                     return square;  
                else  
                     return null;  
           }  
           else{  
                for (int num = 1; num <= square.length; num++) {  
                     square[i] = num;  
                     if(solveSquare(square, i+1) != null)  
                          return square;  
                }  
                return null;  
           }  
      }  
We basically fill one cell at a time, trying all possible numbers in each cell. This search through solution space is depth first, i.e. trying all possibilities at the deepest level and working up from there. So for the 3x3 case, it will try 000000001, then 000000002, and so on... until the checkAnswer() returns a positive result. Note that we only return one possible solution in this case, e.g. for the 3x3 example, running solveSquare(new int[9], 0) returns array [2 8 5 8 5 2 5 2 8].

That interesting solution made me realize that this is too easy, and upon reading the wikipedia article in more detail, I found out that the numbers are "usually distinct integers". This should be easy to fix...
      private boolean numberUsed(int[] square, int i, int num){  
           for (int j = 0; j < i; j++)  
                if(square[j] == num) return true;  
             
           return false;  
      }  
        
      private int[] solveSquare(int[] square, int i){  
           if(i == square.length){  
                if(checkAnswer(square))  
                     return square;  
                else  
                     return null;  
           }  
           else{  
                for (int num = 1; num <= square.length; num++) {  
                     if(numberUsed(square, i, num)) continue;  
                     square[i] = num;  
                     if(solveSquare(square, i+1) != null)  
                          return square;  
                }  
                return null;  
           }  
      }  
We add a new helper function to check if a number has already been used at a preceding depth level. If so, we skip attempting that assignment. The resulting output for the 3x3 example I get is [2 7 6 9 5 1 4 3 8], which is consistent with the Lo Shu square described in the wikipedia article.

Getting All Possible Solutions
I was interested to get all possible solutions, instead of just one, so I modified the code a little bit to take advantage of some Java-specific constructs and collect all possible solutions:
      private void solveSquare(Integer[] square, int i, List<Integer[]> solutions){  
           if(i == square.length){  
                if(checkAnswer(square))  
                     solutions.add(square.clone());  
           }  
           else{  
                for (int num = 1; num <= square.length; num++) {  
                     if(numberUsed(square, i, num)) continue;  
                     square[i] = num;  
                     solveSquare(square, i+1, solutions);  
                }  
           }  
      }  
        
      public static void main(String[] args) {  
           MagicSquare ms = new MagicSquare();  
             
           List<Integer[]> solutions = new LinkedList<Integer[]>();  
           ms.solveSquare(new Integer[9], 0, solutions);  
           for(Integer[] square : solutions){  
                for (int i = 0; i < square.length; i++)  
                     System.out.print(square[i] + " ");  
                System.out.println();  
           }  
      }  
The code actually looks simple this way too. All we had to do is use the Integer object wrapper for the integer arrays, so we could store them in a list. When we reach the deepest level and get a positive checkAnswer(), we add the solution to the list (it is important to clone it here, as the Integer array is just an object reference and the array contents of this solution will get modified as the code goes on to try other combinations). I've also included the tester main method here to show how the recursive function gets initiated and then tested.

The results I got for the 3x3 magic square are:
2 7 6 9 5 1 4 3 8
2 9 4 7 5 3 6 1 8
4 3 8 9 5 1 2 7 6
4 9 2 3 5 7 8 1 6
6 1 8 7 5 3 2 9 4
6 7 2 1 5 9 8 3 4
8 1 6 3 5 7 4 9 2
8 3 4 1 5 9 6 7 2
Which can be obtained through rotations/reflections of the Lo Shu square, as described in the wikipedia article.

Evaluating the Solution
It's now time to cross-check our solution with one provided in the online tutorial. As expected, the solution is very similar, with a few interesting differences:
  • The check for the answer is similarly done in a separate function; however, it is accessed inside the for loop, not as a base condition. I personally prefer having the base condition clearly defined at the beginning of the recursive function, even as expense of elegance.
  • Tracking which numbers have been used is implemented differently and is more efficient than my version. The author cleverly uses another array to check mark numbers (represented by array index) that has already been used.
The author goes on optimizing their baseline solution with various tweaks. Without reading too far in, I want to try some of my own tweaks first.

Optimization
My baseline solution takes 1641ms for 3x3. Using the used-number tracking method from the tutorial reduces these times to 141ms - over factor of 10 improvement! Here is the updated code:
      private void solveSquare(Integer[] square, boolean[] used, int i, List<Integer[]> solutions){  
           if(i == square.length){  
                if(checkAnswer(square))  
                     solutions.add(square.clone());  
           }  
           else{  
                for (int num = 1; num <= square.length; num++) {  
                     if(used[num] == false)  
                          used[num] = true;  
                     else  
                          continue;  
                       
                     square[i] = num;  
                     solveSquare(square, used, i+1, solutions);  
                     used[num] = false;  
                }  
           }  
      }  
Notice that we reset the flag after the recursive call, effectively "unlocking" that number to be used on a different branch (called from the preceding level).

The next optimization we can do is to do some of the checking before we get to the bottom of recursion. So we can check if the rows sum up to the magic constant before we go any deeper. Here is my implementation of this optimization:
      private void solveSquare(Integer[] square, boolean[] used, int i, List<Integer[]> solutions){  
           if(i == square.length){  
                if(checkAnswer(square))  
                     solutions.add(square.clone());  
           }  
           else{  
                for (int num = 1; num <= square.length; num++) {  
                     square[i] = num;  
                       
                     if(used[num] == false){  
                          //check if row sums to magic  
                          if( (i+1) % size == 0){   
                               int sum = 0;  
                               for (int j = i-size+1; j <= i; j++)  
                                    sum += square[j];  
                               if(sum != constant) continue;  
                          }  
                          used[num] = true;  
                     }  
                     else  
                          continue;  
                       
                     solveSquare(square, used, i+1, solutions);  
                     used[num] = false;  
                }  
           }  
      }  
When the depth (variable i) reaches the end of current row, i.e. it is divisible by the size of the square, we check if the row sum works for the current candidate cell assignment. I have two new variables (size and constant) that are declared outside of this function at the construction of the class, since they stay constant for any given magic square (I use these in the checkAnswer() now too, instead of recomputing them every time).

The 3x3 square solutions are now found in 0ms - so that's a further factor of 140 improvement over the baseline!