Linked List (연결 리스트) 란 ?

각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 방식으로 데이터를 저장하는 자료 구조이다. 각 노드는 기본적으로 저장할 "데이터"와 다음 노드가 어디에 있는지 알려주는 "포인터"를 가지고 있고, 이러한 구조가 연결리스트의 연결을 가능하게 한다.

 

Linked List 의 시간 복잡도

일반적으로 앞에서부터 탐색을 하고 찾은 후에 변경하기 때문에 모두 O(n)의 시간 복잡도를 가지고 있다. 하지만 구현하기에 따라 더 효율적으로 만들 수 있다. 맨 앞 노드(head), 맨 뒤 노드(tail)을 이용하는 방법인데 이는 코드로 구현해보겠다.

조회 O(n)
삽입 O(n)
수정 O(n)
삭제 O(n)

Linked List의 종류

단일(단방향) 연결 리스트

각 노드가 포인터 공간을 하나만 가지고 있으며 포인터가 다음 노드(next)를 가리킨다.

 

장점

  • 구조가 단순하고 메모리 사용이 효율적이다 (포인터 하나만 필요하기 때문에)

단점

  • 역방향으로 이동이 불가능하다.
  • 삭제 시 이전 노드에서 다음 노드를 삭제하는 구조라서 삭제할 노드의 앞 노드를 알아야 한다.

단일 연결리스트

이중(양방향) 연결 리스트

각 노드가 두 개의 포인터 공간을 가지고 있고, 각 포인터는 이전(prev), 다음(next) 노드를 가리킨다.

 

장점

  • 양방향 탐색 가능
  • 탐색, 삽입,삭제가 비교적 더 자유로움

단점

  • 비교적 메모리를 더 사용한다.

이중 연결 리스트

 

원형 연결 리스트

마지막 노드와 처음 노드를 연결 시켜(마지막 노드의 포인터가 첫번째 노드를 가리키는) 원형으로 만든 구조이다. 단일, 이중 연결 리스트 모두에서 사용이 가능하다.

 

마지막 노드의 next를 처음 노드로 연결 시켰기 때문에, head를  마지막 노드를 가리키게 하면, 리스트의 처음과 끝에 쉽게 삽입할 수도 있다.

 

장점

  • 어느 노드에서든 순회가 가능하다 (시작 지점이 꼭 head일 필요가 없다.)

단점

  • 무한 루프를 의식해야한다

 

원형 연결 리스트

 

 

구현

연결리스트의 삽입, 삭제, 조회 기능구현

 

단일 연결 리스트

 

Node + linkedList 틀 만들기

class Node {
  next = null;
  constructor(value) {
    this.value = value;
  }
}

class LinkedList {
  head = null;
  length = 0;
  
  add(value) {}
  search(index) {}
  remove(index) {}
}

 

삽입 O(n) : Add 구현

 

add 시 고려할 점 : 비어있는 리스트인가? -> head가 있는지 없는지 조건문으로 판단하여 구현

 

add 메서드에 value를 넣었는데 value를 반환하면 이상할 것 같아서 add할 때마다 리스트의 length를 반환하기로 했다.

class LinkedList {
  head = null;
  length = 0;
  add(value) {
    if(this.head) {
      let current = this.head;
      while(current.next) {
        current = current.next;
      }
      current.next = new Node(value);
    } else {
      this.head = new Node(value);
    }
    this.length++;
    console.log(this.length);
    return this.length
  }
}

 

조회 O(n) : Search 구현

class LinkedList {
  head = null;
  length = 0;

  search(index) {
    let count = 0;
    let current = this.head;
    while(count < index) {
      current = current?.next;
      count++;
    }
    return current?.value
  }
}

 

삭제 O(n) : remove 구현

 

단일 연결리스트의 단점을 위에서 얘기한 것처럼 삭제 시에는 삭제할 index까지 탐색 후 이전 노드를 이용해서 삭제를 해야한다.

 

class LinkedList {
  head = null;
  length = 0;

  remove(index) {
    let count = 0;
    let current = this.head;
    let prev;
    while(count < index) {
      prev = current;
      current = current?.next;
      count++;
    }
    if(prev && current) {
      this.length--;
      prev.next = current.next;
    } else if (current) { 
      this.head = current.next;
      this.length--;
    }
    return this.length;
  }
}

 

코드를 구현하고 보니 remove는 결국 search 이후에 제거를 하는 것이기에 리팩토링을 할 수 있을 것 같다.

 

search, remove 리팩토링

 

linkedlist의 search, remove의 공통되는 코드를 빼내서 private 클래스를 이용하여  #search 메서드를 만들어놓고, search와 remove메서드에서 적절히 쓰면 좋을 것 같다.

class LinkedList {
  head = null;
  length = 0;

  #search(index) {
    let count = 0;
    let current = this.head;
    let prev;
    while(count < index) {
      prev = current;
      current = current?.next;
      count++;
    }
    return [prev, current];
  }
  
  search(index) {
    return this.#search(index)[1]?.value;
  }

  remove(index) {
    const [prev, current] = this.#search(index);
    
    if(prev && current) {
      this.length--;
      prev.next = current.next;
    } else if (current) { 
      this.head = current.next;
      this.length--;
    } 
    return this.length;
  }
}
// 테스트코드
const ll = new LinkedList();

ll.add(1); // length : 1
ll.search(3); // undefined
ll.add(2); // length : 2
ll.add(3); // length : 3
ll.add(4); // length : 4
ll.add(5); // length : 5
ll.search(3); // 4
ll.search(10); // undefined
ll.remove(3) // length : 4
ll.remove(0) // length : 3
ll.search(3); // lundefined

 

이중 연결리스트 

1. Node에 prev 프로퍼티를 만들어놓고 삽입할때마다 prev를 업데이트 해주기 -> 각 노드마다 이전 값을 기억하도록 저장

2. 리스트에 tail 프로퍼티를 추가하여 add 시마다 업데이트하여 삽입을 O(1) 로 만들어보기

3. tail이 추가됨에 따라 remove 시에도 tail을 업데이트 해줘야한다.

 

+ 원형 연결리스트로 가려면 아래 코드에서 tail과 head만 연결 시켜주면 된다.

class Node {
  prev = null;
  next = null;
  constructor(value) {
    this.value = value;
  }
}

// 이전 값 찾기 해보기 Node에 prev
// 삽입을 O(1) 로 만들어보기 tail add할 때마다 tail에 넣으면 되지않을까?


class LinkedList {
  head = null;
  tail = null;
  length = 0;
  add(value) {
    const newNode = new Node(value);
    if(this.head) {
      newNode.prev = this.tail;
      this.tail.next = newNode;
      this.tail = newNode;
    } else {
      this.head = newNode;
      this.tail = newNode;
    }
    this.length++;
    console.log(this.length);
    return this.length
  }

  #search(index) {
    let count = 0;
    let current = this.head;
    let prev;
    while(count < index) {
      prev = current;
      current = current?.next;
      count++;
    }
    return [prev, current];
  }
  search(index) {
    return this.#search(index)[1]?.value;
  }

  remove(index) {
    const [prev, current] = this.#search(index);
    
    if(prev && current) {
      if(!current.next) { // 마지막 node 삭제인 경우
        prev.next = null;
        this.tail = prev;
      } else { // 중간 node 삭제인 경우
        prev.next = current.next;
        current.next.prev = prev; 
      }
      this.length--;
    } else if (current) { // index 가 0일 때
      this.head = current.next;
      this.length--;
    } else {
      // 삭제하고자 하는 대상이 없을 때 아무것도 안함
    }
    return this.length;
  }
}

const ll = new LinkedList();

// 테스트코드
ll.add(1); // length : 1
ll.search(3); // undefined
ll.add(2); // length : 2
ll.add(3); // length : 3
ll.add(4); // length : 4
ll.add(5); // length : 5
ll.search(3); // 4
ll.search(10); // undefined
ll.remove(3) // length : 4
ll.remove(0) // length : 3
ll.search(3); // lundefined

 

문제 링크

 

https://school.programmers.co.kr/learn/courses/30/lessons/17681

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

나의 풀이

function solution(n, arr1, arr2) {
    const answer = [];
    const binary1 = arr1.map((v)=>{
        v = v.toString(2);
        while(v.length < n){
            v = "0" + v   
        }
        
        return v
    });
    const binary2 = arr2.map((v)=>{
        v = v.toString(2);
        while(v.length < n){
            v = "0" + v   
        }
        
        return v
    });

    for(let i=0; i<n; i++) {
        let str = "";
        for(let j=0; j<n; j++){
            const m = binary1[i][j];  
            const n = binary2[i][j];  
            if(m==="0" && n==="0") str+=" "
            else str+="#"
        }
        
        answer.push(str)
    }
    
    return answer
}

 

주어진 배열을 모두 이진수로 바꾸고 반복문을 이용해서 풀었다.

 

이진수로 바꿨을 때 앞에 0이 안붙는 문제가 생겨서 이진수로 변경중간에 while문을 통해서 n만큼 0을 붙여주는 작업을 했다.

그래서 코드 효율이 좋지 않았다.

 

다른 사람의 풀이

function solution(n, arr1, arr2) {
    return arr1.map((v, i) => addZero(n, (v | arr2[i]).toString(2)).replace(/1|0/g, a => +a ? '#' : ' '));
}

const addZero = (n, s) => {
    return '0'.repeat(n - s.length) + s;
}

 

repeat는 생각 못했다. n-s.length를 이용한 것도 while 문을 없앨 수 있는 좋은 방법인 것 같다.

문자열이 있다고 치자. 그리고 이 문자열의 각 문자들이 특정 문자와의 거리(가까운 거리)를 구해야 한다.

 

이해하기 쉽게 예시로 teachermode라는 문자열이 있고 특정 문자 e가 입력되면, 출력은

1 0 1 2 1 0 1 2 2 1 0 이다.

 

나는 처음에 문자열의 각 문자를 같은 문자열의 다른 문자와 비교해야하므로 무조건 이중 for문을 써야할 수 밖에 없다고 생각해서 아래와 같이 풀었다.

function solution(s,t){
  const answer = [];
  for(let i=0; i<s.length; i++){
    let min_distance = Number.MAX_SAFE_INTEGER;
    for(let j=0; j<s.length; j++){
      if(s[j] === t){
        const distance = Math.abs(i-j);
        if(distance < min_distance) min_distance = distance;
      }
    }
    answer.push(min_distance);
  }

  return answer.join(" ")
}

 

 

IDEA : 왼쪽에서부터의 거리와 오른쪽에서부터의 거리를 각각 검사하고, 작은것들로 취합.

function solution(s,t) {
  const answer = [];
  let p = 100;
  for(let i=0; i<s.length; i++){
    if(s[i] === t) {
      p = 0;
      answer.push(p);
    } else {
      p++;
      answer.push(p);
    }
  }

  p = 100;
  for(let i=s.length-1; i>=0; i--){
    if(s[i] === t) p=0;
    else {
      p++;
      answer[i] = Math.min(p, answer[i]); // 원래있던 왼쪽에서와의 거리와 현재 오른쪽에서와의 거리중 작은값
    }
  }
  return answer.join(" ")
}

아스키코드

언젠가 쓸 일이 있으니 소문자 a-z 부터 대문자 A-Z까지는 외워두자.

 

대문자 알파벳 : 65~90 까지 26개
소문자 알파벳 : 97~122 까지 26개

 

charCodeAt(), String.fromCharCode()

// 문자의 아스키코드 체크하는 법
'A'.charCodeAt() // 65

// 반대로 아스키코드로 문자 체크하는 법
String.fromCharCode(65) // 'A'

 

replace 로 문자열 바꾸기 + 정규표현식

replace(a,b) -> a를 b로 바꾼다.

 

정규표현식을 이용하면 /A/를 #으로 바꾼다. 다만, A를 만나면 #으로 바꾸고 아예 끝나버리기 때문에 문자열 전체에서 A를 #으로 바꾸려면 g(global)를 붙여준다.

function solution(str) {
  let answer = str;
  str = str.replace('/A/g', '#');
  return answer
}

console.log(solution("BANANA"));

 

정규표현식을 이용하면 문자열에서 알파벳들만 걸러낼 수 있다.

function solution(str) {
  const answer = s.replace("/[^a-z]/g","")
  return answer
}

const s = 'ab ;c, dA'
console.log(solution(s)) // abcd

 

문자열에서 숫자만 걸러내기 + 028 과 같은 문자열 28숫자로 나타내기 원리

 

문자열이 있을 때 숫자만 걸러낼 수 있는 방법

 

isNaN : 문자열이 NaN(숫자가아니면) true 숫자이면 false

function solution(str) {
  let numberStr = "";
  for(let i=0; i<str.length; i++){
    if(!isNaN(str[i])) numberStr+=str[i]
  }
}

const s = 'a0b1 ;c, dA23'

console.log(solution(s)) // "0123"

 

여기서 0123을 123으로 바꾸려면 parseInt, Number, *1 을 해주는 여러가지 내장함수들을 사용했었다. 근데 직접 구현해보자.

어떤 숫자이던 일의자리 숫자 전까지 10을 곱하고 일의자리만 더해주면 그 숫자가 나온다.

즉 0123 이면 10*0+0 = 0, 1*10+2, 12*10+3 이런식으로 표현할 수 있다.

function solution(str) {
  let numberStr = 0;
  for(let i=0; i<str.length; i++){
    if(!isNaN(str[i])) numberStr = numberStr*10 + Number(str[i])
  }
}

const s = 'a0b1 ;c, dA23'

console.log(solution(s)) // "123"

 

알고리즘 문제를 풀다 보면, 최댓값이나 최솟값을 구해야 하는 문제가 종종 있다.

 

나는 지금까지 최솟값을 구할 때나 최댓값을 구할 때 어떤 값으로 초기화 해야할 지 고민하는데 시간을 썼다.

 

쉬운 문제라면 최솟값이나 최댓값의 범위를 알려줘서 (ex. 자연수일 때 최솟값 0으로 초기화) 고민이 없었는데, 어려운 문제를 풀다 보면 설정하기 애매한 조건들이 있었다.

 

이 방법을 왜 이제 알았을까?

 

최소값 초기화 Number.MAX_SAFE_INTEGER

어떤 입력값들을 탐색할 때 최솟값을 가장 크게 해놓는다면, 입력값의 처음 인덱스 혹은 첫 값부터 최솟값이 되고, 이후 모두 탐색할 수 있다.

 

  let min = Number.MAX_SAFE_INTEGER;
  for(let i=0; i<arr.length; i++){
    if(arr[i] < min) {
      min = arr[i];
    }
  }

 

 

최대값 초기화 Number.MIN_SAFE_INTEGER

마찬가지로 최댓값은 반대로 최댓값을 가장 작은 값으로 초기화한다면, 첫 값부터 최댓값이되고, 모두를 탐색할 수 있다.

  let max = Number.MIN_SAFE_INTEGER;
  for(let i=0; i<arr.length; i++){
    if(arr[i] > max) {
      max = arr[i];
    }
  }

+ Recent posts