This is overall n^3, but that's meaningless because there actually isn't just one n. So, more m^2 * n. That means you can't reduce it to anything, because you want to keep both components. (Because, say, you know it will only ever be called with a single string).
But then, in the next app, this gets called and reinitialized once per file. And the routine handling files, for reasons beyond our ken, is (n lg n). We're now at k * log(k) * m^2 * n.
And so, over any sufficiently long call chain, "what is n" is the overriding question - string length, number of strings, number of files? Not "how complex is the algorithm", because you want to optimize for what's relevant to your use case.
The trick is knowing where the nested loops are since they can be spread across functions.
I had a function that scaled as N^2, but it was creating a list of that size as well. Then it called a function to remove duplicates from that list. That function was N^2, which meant the whole thing was actually N^4. And now that I think of it, those loops were not nested... I rewrote the first part to no create duplicates and deleted the quadratic deduplication. Now its N^2, but it has to be.
Here's another crazy idea: keeping track of this while taking into consideration aggressive compiler optimizations.
[1] https://cstheory.stackexchange.com/questions/5004/are-runtim...
def collatz(n):
while n != 1:
print(n)
if n % 2 == 0:
n = n // 2
else:
n = n * 3 + 1
print(1) def collatz(n):
while n != 1:
print(n)
if n % 2 == 0:
n = n // 2
else:
n = n * 3 + 1
print(1)
Otherwise, n = 1 terminates, and n != 1 gets stuck looping at lines 2-3.[0]: https://en.wikipedia.org/wiki/Primitive_recursive_function