how to do the regex thing, but actually

(As usual, ensure)

Then, eliminate non-accept/start states one by one, until only start and accept are left.

Eliminating a state:
For all pairs of in-arrows and out-arrows, add a new equivalent “bypass” arrow – the concatenation of the in and the out.
If the state has a self-loop, then sandwich that (with a Kleene star) in the concatenation.

Observe that the number of new arrows to add is #ins ×\times #outs, so eliminate states with the least in×\timesout first!!!

That’s it.


Example

Consider the following 6-state state diagram.

ε
b
b
a
b
a
a
ε
Q0
Q1
Q2
Q3

Q2\rm{Q_2} has 2 ins and 2 outs; to eliminate it would require 4 additional bypass arrows.
Meanwhile, Q0\rm{Q_0} has only 2×\times1 = 2, so let’s eliminate Q0\rm{Q_0} first.

The two in-out pairs are
startundefinedεQ0undefinedbQ1\text{start} \xrightarrow{\varepsilon} \rm{Q_0} \xrightarrow{b} \rm{Q_1}
Q2undefinedaQ0undefinedbQ1\rm{Q_2} \xrightarrow{a} \rm{Q_0} \xrightarrow{b} \rm{Q_1}

We therefore add the bypass arrows
startundefinedbQ1\text{start} \xrightarrow{b} \rm{Q_1}
Q2undefinedabQ1\rm{Q_2} \xrightarrow{ab} \rm{Q_1}

And we can now safely kill Q0\rm{Q_0} (and its attached arrows).

We now have

b
ab
b
a
b
a
ε
Q1
Q2
Q3

Oh no! we have two parallel arrows from Q2\rm{Q_2} to Q1\rm{Q_1}, undefinedb\xrightarrow{\rm b} and undefinedab\xrightarrow{\rm ab}?!
Well, they can be union’d into undefinedb  ab\xrightarrow{\rm b \ |\ ab}:

b
b|ab
b
a
a
ε
Q1
Q2
Q3

Now we repeat the process. Q3\rm{Q_3} (or Q2\rm{Q_2}) now has the least in×\timesout, so let’s eliminate it.

The 2 in-out pairs:
Q1undefinedaQ3undefinedaQ2\rm{Q_1} \xrightarrow{a} \rm{Q_3} \xrightarrow{a} \rm{Q_2}
Q1undefinedaQ3undefinedεend\rm{Q_1} \xrightarrow{a} \rm{Q_3} \xrightarrow{\varepsilon} \text{end}

Turns into:
Q1undefinedaaQ2\rm{Q_1} \xrightarrow{aa} \rm{Q_2}
Q1undefinedaend\rm{Q_1} \xrightarrow{a} \text{end}

So,

b
b|ab
b
aa
a
Q1
Q2

And union’ing undefinedb\xrightarrow{\rm b} and undefinedaa\xrightarrow{\rm aa},

b
b|ab
b|aa
a
Q1
Q2

Now, eliminate Q2\rm{Q_2}.

It only has 1 in-out pair:
Q1undefinedb  aaQ2undefinedb  abQ1\rm{Q_1} \xrightarrow{b\ |\ aa} \rm{Q_2} \xrightarrow{b\ |\ ab} \rm{Q_1}

So we add
Q1undefined(b  aa)(b  ab)Q1\rm{Q_1} \xrightarrow{(b\ |\ aa) (b\ |\ ab)} \rm{Q_1}
(remember to add parentheses)

b
(b|aa)(b|ab)
a
Q1

Only Q1\rm{Q_1} is left 👀.

It has 1 in and 1 out, but has a self-loop!
startundefinedbQ1(baa)(bab)undefinedaend\text{start} \xrightarrow{b} \underset{\mathclap{\overset{\small\circlearrowright}{(b|aa)(b|ab)}}}{\rm{\normalsize Q_1}} \xrightarrow{a} \text{end}

We do the same as before, but sandwich the self-loop along a kleene star:
startundefinedb ((baa)(bab)) aend\text{start} \xrightarrow{\small b \ {\normalsize(}(b|aa)(b|ab){\normalsize)}^* \ a} \text{end}

Which gives

b ((b|aa)(b|ab))* a

…and  b((baa)(bab))a \ \rm b \,\big( (b\,|\,aa)(b\,|\,ab) \big)^*\, a \space is precisely the equivalent regex!