I think the best solution here involves dynamic programming, a difficult concept to grasp, but very efficient if done correctly. The idea is to minimize the number of times you have to do a certain calculation, by remembering the results of calculations you've already done, and then using this knowledge to split the problem into a subset of simpler problems.
For example, in your 2^(2^2) = (2^2)^2 example, we can now remember this as two things that are equivalent to the value of 16. So now when we see this tacked on to 2^(2^(2^2)) = 2^((2^2)^2) we can very quickly identify each of these as 2 ^ 16, do the calculation once, and we now have two new equations to add to the list of values we never have to calculate again.
This may not seem to help much, however, when you end up with very long series of parenthesized questions, with even longer sets of equivalencies, it will save you a of time and complexity, in what are very long calculations for a computer to do on very large numbers. Pseudo code below, I appologize, this is really broad psuedo code, this problem is pretty rough, so I didn't want to write out an entire algorithm. Just really outlined the concept in more detail
sortedEquivalencyVector; //Sorted greatest first, so we are more likely to se longest matches
function(expression)
if(portion of expression exists) //Remember, you can only do this from the end of the expression in toward the middle!
replace value at end of expression that matches with its already computed value
if(simplified version exists in vector) add original expression to vector
else compute value and add it to the vector
end