- Fix your query so you don't need to use DISTINCT. The problem with your method is that you are using a hierarchical query with multiple rows of input and no way of correlating each level of the hierarchy to the previous level so the query will correlate it to ALL the items at the previous level of the hierarchy and you will get exponentially more and more duplicate rows generated at each depth. This is incredibly inefficient.
- Change from using regular expressions to simple string functions.
Instead you can use:
WITH bounds ( class_id, class_time, start_pos, end_pos ) AS (
  SELECT class_id,
         class_time,
         1,
         INSTR( class_time, ':', 1 )
  FROM   data
UNION ALL
  SELECT class_id,
         class_time,
         end_pos + 1,
         INSTR( class_time, ':', end_pos + 1 )
  FROM   bounds
  WHERE  end_pos > 0
)
SELECT class_id,
       CASE end_pos
       WHEN 0
       THEN SUBSTR( class_time, start_pos )
       ELSE SUBSTR( class_time, start_pos, end_pos - start_pos )
       END AS class_time
FROM   bounds;
Which, for the sample data:
CREATE TABLE data ( class_id, class_time ) AS
SELECT 1, '0800AM:0830AM' FROM DUAL UNION ALL
SELECT 1, '0900AM' FROM DUAL UNION ALL
SELECT 2, '0830AM:0900AM:0930AM' FROM DUAL UNION ALL
SELECT 2, '1000AM' FROM DUAL;
Outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
       1 | 0800AM    
       1 | 0900AM    
       2 | 0830AM    
       2 | 1000AM    
       1 | 0830AM    
       2 | 0900AM    
       2 | 0930AM    
db<>fiddle here
However, an even better method would be to change your model for storing the data and stop storing it as a delimited string and instead store it in a separate table or, maybe, as a collection in a nested table.
An example using a second table is:
CREATE TABLE data (
  class_id   NUMBER PRIMARY KEY
);
CREATE TABLE class_times (
  class_id   NUMBER REFERENCES data ( class_id ),
  class_time VARCHAR2(6)
);
INSERT ALL
  INTO data ( class_id ) VALUES ( 1 )
  INTO data ( class_id ) VALUES ( 2 )
  INTO class_times ( class_id, class_time ) VALUES ( 1, '0800AM' )
  INTO class_times ( class_id, class_time ) VALUES ( 1, '0830AM' )
  INTO class_times ( class_id, class_time ) VALUES ( 1, '0900AM' )
  INTO class_times ( class_id, class_time ) VALUES ( 2, '0830AM' )
  INTO class_times ( class_id, class_time ) VALUES ( 2, '0900AM' )
  INTO class_times ( class_id, class_time ) VALUES ( 2, '0930AM' )
  INTO class_times ( class_id, class_time ) VALUES ( 2, '1000AM' )
SELECT * FROM DUAL;
Then your query would be (assuming that you need other columns from data alongside the class_id):
SELECT d.class_id,
       c.class_time
FROM   data d
       INNER JOIN class_times c
       ON ( d.class_id = c.class_id );
Which outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
       1 | 0800AM    
       1 | 0830AM    
       1 | 0900AM    
       2 | 0830AM    
       2 | 0900AM    
       2 | 0930AM    
       2 | 1000AM    
An example using a nested table is:
CREATE TYPE stringlist IS TABLE OF VARCHAR2(6);
CREATE TABLE data (
  class_id   NUMBER,
  class_time stringlist
) NESTED TABLE class_time STORE AS data__class_time;
INSERT INTO data ( class_id, class_time )
SELECT 1, stringlist( '0800AM','0830AM' ) FROM DUAL UNION ALL
SELECT 1, stringlist( '0900AM' ) FROM DUAL UNION ALL
SELECT 2, stringlist( '0830AM','0900AM','0930AM' ) FROM DUAL UNION ALL
SELECT 2, stringlist( '1000AM' ) FROM DUAL;
Then your query would become:
SELECT d.class_id,
       ct.COLUMN_VALUE AS class_time
FROM   data d
       CROSS APPLY TABLE ( d.class_time ) ct
Which outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
       1 | 0800AM    
       1 | 0830AM    
       1 | 0900AM    
       2 | 0830AM    
       2 | 0900AM    
       2 | 0930AM    
       2 | 1000AM    
db<>fiddle here