Я попытался решить это сам и нашел другое (менее эффективное, но все же оптимальное) решение.
Идея основана на обращении односвязного списка за линейное время. Это можно сделать, выполнив два перестановки на каждом шаге итерации по списку. Если q - предыдущий элемент (изначально ноль), а p - текущий, то swap (q, p-> next) swap (p, q) обратит ссылку и продвинет два указателя одновременно. Перестановки можно выполнить с помощью XOR, чтобы избежать необходимости использования третьей области памяти.
Если в списке есть цикл, то в один момент итерации вы попадете на узел, указатель которого уже был изменен. Вы не можете знать, какой это узел, но, продолжая итерацию, меняя местами два элемента, вы снова попадаете в начало списка.
При повторном обращении к списку список остается неизменным в результате, и вы можете определить, имел ли он цикл в зависимости от того, достигли ли вы первоначального заголовка списка или нет.