4주차 1차 1 - LeetCode-Study-Team/solutions GitHub Wiki

141 Linked List Cycle

용어 정리

연결 리스트(LinkedList)

각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 방식의 자료구조입니다

장점과 단점

중간에 데이터를 추가나 삭제하더라도 전체의 인덱스가 한 칸씩 뒤로 밀리거나 당겨지는 일이 없기에 ArrayList에 비해서 데이터의 추가나 삭제가 용이하나, 인덱스가 없기에 특정 요소에 접근하기 위해서는 순차 탐색이 필요로 하여 탐색 속도가 떨어진다는 단점이 있습니다.

linkedlist.png

출처 [코딩팩토리] 자바 LinkedList 사용법 & 예제 총정리

https://coding-factory.tistory.com/552

문제 요약

주어진 head, linked list 에 사이클이 있는지 확인합니다.

pos 는 파라미터로 전달되지 않습니다.

linked list 에 사이클이 있으면 true , 아니면 false 를 반환

circularlinkedlist.png

Constraints:

The number of the nodes in the list is in the range [0, 10^4].

-10^5 <= Node.val <= 10^5

pos 는  -1 또는 linked-list 에서 유효한 인덱스

Follow up: Can you solve it using O(1) (i.e. constant) memory?

class ListNode

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */

Discuss 01

[HT_Wang]
I don't see why we need 'pos' in the inputs, 
while we don't see it in the code. It's kind confusing.

[alexidealex]
When I read the description I was going to write the solution like this:

if (p > 0)
    return true;
else
    return false;

Discuss 02

[fabrizio3]
/**
 1. Use two pointers, walker and runner.
 2. walker moves step by step. runner moves two steps at time.
 3. if the Linked List has a cycle walker and runner will meet at some point.
 */

public boolean hasCycle(ListNode head) {
    if(head==null) return false;
    ListNode walker = head;
    ListNode runner = head;
    while(runner.next!=null && runner.next.next!=null) {
        walker = walker.next;
        runner = runner.next.next;
        if(walker==runner) return true;
    }
    return false;
}
[jianchao-li]
The if(head==null) check can be further removed.
while(runner!=null && runner.next!=null)

Solution

Approach 1: Hash Table

public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> nodesSeen = new HashSet<>();
        while (head != null) {
            if (nodesSeen.contains(head)) {
                return true;
            }
            nodesSeen.add(head);
            head = head.next;
        }
        return false;
    }
}

Time complexity : O(n)
Space complexity : O(n)

HashSet 메소드의 시간복잡도 add : O(1) contains : O(1) next : o(h/n), h는 테이블 용량

출처 [GrepIU] JAVA Collection 시간 복잡도/특징 정리

https://www.grepiu.com/post/9

Approach 2: Floyd's Cycle Finding Algorithm

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null) {
            return false;
        }

        ListNode slow = head;
        ListNode fast = head.next;
        while (slow != fast) {
            if (fast == null || fast.next == null) {
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

Time complexity : O(n)
Space complexity : O(1). 
We only use two nodes (slow and fast) so the space complexity is O(1).

207 Course Schedule

용어 정리

백트래킹(BackTracking)

해를 찾아가는 도중에 해가 될 것 같지 않으면 되돌아간다.

이를 가지치기라고 하고 가지치기를 얼마나 잘하느냐에 따라 효율성을 결정한다.

위상 정렬(Topological sort)

어떤 일을 하는 순서를 찾는 알고리즘이다.

즉, 방향 그래프에 존재하는 각 정점들의 선행 순서를 위배하지 않으면서 모든 정점을 나열하는 것

위상 정렬을 이용한 기본적인 해결 방법

topological-sort-example.png

  1. 진입 차수가 0인 정점(즉, 들어오는 간선의 수가 0)을 선택

    • 진입 차수가 0인 정점이 여러 개 존재할 경우 어느 정점을 선택해도 무방하다.
    • 초기에 간선의 수가 0인 모든 정점을 큐에 삽입
  2. 선택된 정점과 여기에 부속된 모든 간선을 삭제

    • 선택된 정점을 큐에서 삭제
    • 선택된 정점에 부속된 모든 간선에 대해 간선의 수를 감소
  3. 위의 과정을 반복해서 모든 정점이 선택, 삭제되면 알고리즘 종료

출처

[heejeong Kwon] 알고리즘 위상 정렬(Topological Sort)이란

[ChanBlog] 알고리즘 - 백트래킹(Backtracking)의 정의 및 예시문제

https://chanhuiseok.github.io/posts/algo-23/ https://gmlwjd9405.github.io/2018/08/27/algorithm-topological-sort.html

문제 요약

수강해야 하는 총 과정 : numCourses

강의 번호 : 0 to numCourses - 1.

배열 prerequisites[i] = [ai, bi] : ai 과정을 수강하려면 bi 과정을 먼저 수강해야 함을 나타냄

모든 과정을 마칠 수 있으면 true , 없으면 false.

Constraints:

1 <= numCourses <= 105

0 <= prerequisites.length <= 5000

prerequisites[i].length == 2

0 <= ai, bi < numCourses

모든 쌍의 prerequisites[i] 는 고유하다.

백트래킹

backtracking.png

유망하지 않은 것 : 루프(싸이클)이 있는 것

문제 풀이

Approach 1: Backtracking

class Solution {
  public boolean canFinish(int numCourses, int[][] prerequisites) {

    // 코스, 다음 코스 목록
    HashMap<Integer, List<Integer>> courseDict = new HashMap<>();

    // 그래프 빌드
    for (int[] relation : prerequisites) {
      // relation[0]이 relation[1]에 종속 : relation[0] <- relation[1]
      if (courseDict.containsKey(relation[1])) {  //ex. [0,1]
        courseDict.get(relation[1]).add(relation[0]);
      } else {
        List<Integer> nextCourses = new LinkedList<>();
        nextCourses.add(relation[0]);
        courseDict.put(relation[1], nextCourses);
      }
    }

    boolean[] path = new boolean[numCourses];

    for (int currCourse = 0; currCourse < numCourses; ++currCourse) {
      if (this.isCyclic(currCourse, courseDict, path)) {
        return false;
      }
    }

    return true;
  }


  /*
   * 백트래킹 메소드 : currCourse에서 시작하여 사이클이 형성되지 않는지 체크하는 메소드
   */
  protected boolean isCyclic(
      Integer currCourse,
      HashMap<Integer, List<Integer>> courseDict,
      boolean[] path) {

    if (path[currCourse]) {
      // 이전 방문한 노드를 발견한다. 즉, 사이클을 감지한다
      return true;
    }

    // 다음 과정이 없음, 루프가 없음
    if (!courseDict.containsKey(currCourse))
      return false;

    // 백트래킹 하기 전, path에 노드를 표시
    path[currCourse] = true;

    // 백트래킹
    boolean ret = false;
    for (Integer nextCourse : courseDict.get(currCourse)) {
      ret = this.isCyclic(nextCourse, courseDict, path);
      if (ret)
        break;
    }
    // 백트래킹 한 이후, path에서 노드 제거
    path[currCourse] = false;
    return ret;
  }
}

E는 종속성의 수, V는 코스의 수
시간 복잡도 : O(E+V^2) 
공간 복잡도 : O(E+V)

Approach 2: Postorder DFS (Depth-First Search)

class Solution {
  public boolean canFinish(int numCourses, int[][] prerequisites) {

    // course -> list of next courses
    HashMap<Integer, List<Integer>> courseDict = new HashMap<>();

    // build the graph first
    for (int[] relation : prerequisites) {
      // relation[0] depends on relation[1]
      if (courseDict.containsKey(relation[1])) {
        courseDict.get(relation[1]).add(relation[0]);
      } else {
        List<Integer> nextCourses = new LinkedList<>();
        nextCourses.add(relation[0]);
        courseDict.put(relation[1], nextCourses);
      }
    }

    boolean[] checked = new boolean[numCourses];
    boolean[] path = new boolean[numCourses];

    for (int currCourse = 0; currCourse < numCourses; ++currCourse) {
      if (this.isCyclic(currCourse, courseDict, checked, path))
        return false;
    }

    return true;
  }


  /*
   * postorder DFS check that no cycle would be formed starting from currCourse
   */
  protected boolean isCyclic(
      Integer currCourse, HashMap<Integer, List<Integer>> courseDict,
      boolean[] checked, boolean[] path) {

    // bottom cases
    if (checked[currCourse])
      // this node has been checked, no cycle would be formed with this node.
      return false;
    if (path[currCourse])
      // come across a previously visited node, i.e. detect the cycle
      return true;

    // no following courses, no loop.
    if (!courseDict.containsKey(currCourse))
      return false;

    // before backtracking, mark the node in the path
    path[currCourse] = true;

    boolean ret = false;
    // postorder DFS, to visit all its children first.
    for (Integer child : courseDict.get(currCourse)) {
      ret = this.isCyclic(child, courseDict, checked, path);
      if (ret)
        break;
    }

    // after the visits of children, we come back to process the node itself
    // remove the node from the path
    path[currCourse] = false;

    // Now that we've visited the nodes in the downstream,
    // we complete the check of this node.
    checked[currCourse] = true;
    return ret;
  }
}

Approach 3: Topological Sort

class GNode {
  public Integer inDegrees = 0;
  public List<Integer> outNodes = new LinkedList<Integer>();
}


class Solution {

  public boolean canFinish(int numCourses, int[][] prerequisites) {

    if (prerequisites.length == 0)
      return true; // no cycle could be formed in empty graph.

    // course -> list of next courses
    HashMap<Integer, GNode> graph = new HashMap<>();

    // build the graph first
    for (int[] relation : prerequisites) {
      // relation[1] -> relation[0]
      GNode prevCourse = this.getCreateGNode(graph, relation[1]);
      GNode nextCourse = this.getCreateGNode(graph, relation[0]);

      prevCourse.outNodes.add(relation[0]);
      nextCourse.inDegrees += 1;
    }

    // We start from courses that have no prerequisites.
    int totalDeps = prerequisites.length;
    LinkedList<Integer> nodepCourses = new LinkedList<Integer>();
    for (Map.Entry<Integer, GNode> entry : graph.entrySet()) {
      GNode node = entry.getValue();
      if (node.inDegrees == 0)
        nodepCourses.add(entry.getKey());
    }

    int removedEdges = 0;
    while (nodepCourses.size() > 0) {
      Integer course = nodepCourses.pop();

      for (Integer nextCourse : graph.get(course).outNodes) {
        GNode childNode = graph.get(nextCourse);
        childNode.inDegrees -= 1;
        removedEdges += 1;
        if (childNode.inDegrees == 0)
          nodepCourses.add(nextCourse);
      }
    }

    if (removedEdges != totalDeps)
      // if there are still some edges left, then there exist some cycles
      // Due to the dead-lock (dependencies), we cannot remove the cyclic edges
      return false;
    else
      return true;
  }

  /**
   * Retrieve the existing <key, value> from graph, otherwise create a new one.
   */
  protected GNode getCreateGNode(HashMap<Integer, GNode> graph, Integer course) {
    GNode node = null;
    if (graph.containsKey(course)) {
      node = graph.get(course);
    } else {
      node = new GNode();
      graph.put(course, node);
    }
    return node;
  }
}
⚠️ **GitHub.com Fallback** ⚠️