Husk, 44 43 41 40 bytes
|s0Ψf¤|□ṁ`:'+f¹zμ+↓s²_&ε²¹↑□¹+"x^"s)¹m←ṡ This feels a bit clunky; Husk is not optimized for string manipulation. I'll try to golf it down. I borrowed some ideas from the Stax answer.
Explanation
Implicit input, say L = [2,-3,0,-1]. First we compute the exponents. ṡ Reversed indices: [4,3,2,1] m← Decrement each: [3,2,1,0] Then we format the individual terms of the polynomial. zμ...)¹ Zip with L using two-argument lambda: Arguments are coefficient and index, say C = -3 and I = 2. +"x^"s Convert I to string and concatenate to "x^": "x^2" ↑□¹ Take first I*I characters (relevant when I = 0 or I = 1): "x^2" _&ε²¹ Check if abs(C) <= 1 and I != 0, negate; returns -1 if true, 0 if false. ↓s² Convert C to string and drop that many elements (from the end, since negative). Result: "-3" The drop is relevant if C = 1 or C = -1. + Concatenate: "-3x^2" Result of zipping is ["2x^3","-3x^2","x","-1"] f¹ Keep those where the corresponding element of L is nonzero: ["2x^3","-3x^2","-1"] Next we join the terms with + and remove extraneous +s. ṁ Map and concatenate `:'+ appending '+': "2x^3+-3x^2+-1+" Ψf Adjacent filter: keep those chars A with right neighbor B ¤|□ where at least one of A or B is alphanumeric: "2x^3-3x^2-1" |s0 Finally, if the result is empty, return "0" instead.