문제 : 두 개의 비어있지 않은 연결 리스트가 주어집니다. 이 리스트들은 각각 하나의 비음수 정수를 나타내며, 각 노드는 한 자리 숫자를 포함하고 있습니다. 숫자는 역순으로 저장되어 있습니다. 두 숫자를 더한 후, 그 합을 연결 리스트로 반환하세요.

두 숫자는 숫자 0 자체를 제외하고는 앞에 0이 포함되어 있지 않다고 가정할 수 있습니다.

 

예시 :

 

즉, 두개의 연결 리스트가 주어지고, 그 연결 리스트를 처음  tail부터 head까지(역순) 연속된 노드들을 이어 정수를 만들고, 그 두 값을 더해 새로운 연결 리스트로 리턴 해야한다.

 

1. 처음 나의 생각 : 

일단 연결 리스트가 주어졌고, 위의 예시 첫번째를 보면 2 -> 4 -> 3이다. 이 연결 리스트를 뒤에서부터 읽으면 342이다. 자 그럼, 이 연결 리스트를 어떻게 342라는 정수로 표현 할 수 있을까?

 

342 = 300 + 40 + 2

 

즉, 초기 자릿수 값을 1로 설정하고, 노드를 넘어갈 때마다 그 자릿수에 10을 곱해 정수를 만든다. 예를 들어, head부터 읽으면 2가 읽힐것이다. 이때, 2와 초기 자릿수값 1을 곱해 sum1(첫번째 인자로 주어지는 연결리스트의 합)이라는 변수에 2를 더하고, 그 다음 노드로 넘어가면 4를 읽고, 이 4와 업데이트된 자릿수 값 10을 더해 40을 sum1에 더한다.(2 + 40 = 42) 마지막으로, 그 다음 노드의 값인 3을 읽어와 업데이트된 자릿수 값 100이랑 곱한다(300 + 40 + 2). 

 

두번째 연결리스트 인자도 같은 방식으로 총합을 계산 한 후, 총합이 1보다 클때까지 나머지 연산(modulo)를 이용해 각 자릿수의 값을 가져온다. 그 다음, 그 값을 다시 새로운 연결 리스트에 담아 반환한다.

 

코드는 아래와 같다.

 

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {

        ListNode dummy = new ListNode(0);
        ListNode rCurrent = dummy;

        ListNode current = l1; 
        ListNode current2 = l2; 

        int l1Sum = 0;
        int l2Sum = 0;
        int digit = 1;
        int digit2 = 1;

        while(current != null) {
            l1Sum += current.val * digit;
            current = current.next;
            digit *= 10;
        }

        while(current2 != null) {
            l2Sum += current2.val * digit2;
            current2 = current2.next;
            digit2 *= 10;
        }

        int total = l1Sum + l2Sum;

        if (total == 0) {
            return new ListNode(0);
        }

        while(total > 0) {
            int digits = total % 10;
            rCurrent.next = new ListNode(digits);
            rCurrent = rCurrent.next;
            total /= 10;
        }
        return dummy.next;
    }
}

 

 

일단 여기서 연결 리스트와 관련된 궁금증 몇가지를 풀고 가 보자. 

 

  • dummy : 말 그대로 연결 리스트의 첫번째 값을 참조하기 위해 만든 임시 노드이다. 생성자에 0을 넘겨, 그냥 임시 노드(시작노드)를 만든 것이다. 이 dummy는 나중에 최종 연결 리스트를 반환할때 사용된다. 예를들어, 우리가 만약 1->2->3을 반환해야 한다면, dummy노드를 이용해 0->1->2->3 와 같은 연결 리스트를 만들고, 앞의 0을 제외한 나머지 1->2->3 을 반환해주면 된다.
  • dummy.next : 왜 next를 반환할지부터 생각 해 보자. 맨 처음에 dummy 노드는 노드의 값을 0으로 초기화 해 주었다. 하지만 이 더미 노드는 말 그대로 노드의 시작점을 참조하기 위해 우리가 임의로 만든 것이고, 여기에 dummy.next를 하면 뒤의 값이 모두 반환된다. 연결 리스트의 특징 :. .next로 다음 노드를 참조하면, 다음 노드 뿐만 아니라 next로 연결되어있는 노드까지 모두 반환한다.

하지만... 기본적인 테스트 케이스 몇개를 제외하곤 너무나도 많은 테스트 케이스를 통과하지 못 했다. 문제를 보니,,, 테스트 케이스로 준 연결 리스트는 제일 긴 총 합이 899,999,999였다. 그런데 테스트 케이스는 int로 커버가 불가능한 long의 영역까지 커버해야하는것을 알고, 형 변환을 해 주었다. 하지만 이번에는 1884개의 테스트중 3개의 테스트가 또 다시 fail하였다. 문제를 보니 이건 int나 long의 문제가 아니였다. 내가 너무 단순한 수학적인 방식으로 풀려고 했기 때문에 실패했던 것이였다.

 

2. 두번째 생각 : 그럼 type문제를 아예 빼놓고, 이 문제를 어떻게 해결 할 수 있을까?

 

아이패드에 연결 리스트 그림을 그려놓고 보니, 그냥 각 자릿수의 노드들을 서로 더해 같은 자릿수에 넣어주면 되는 것이였다... 굳이 복잡하게 일일히 1, 10, 100을 곱해주면서까지 정수를 만들고, 그 정수들을 다시 더하고 나머지 연산을 하는 등 복잡한 로직은 전혀 필요하지 않았다.

 

예를들어, 2->4->3 && 5->6->4는 (2+5) (4+6) (3+4)를 각각 그에 맞는 자릿수에 넣어주면 됐다. 하지만 여기서 문제가 하나 생긴다. 가운데 4 +6 처럼 더한 값이 10이 넘어버리면 어떻게 해야 할까?(당연히 손으로 덧셈을 하면 어떻게 해야할지 안다. 하지만 이걸 코드로 어떻게 풀어낼까?) 

 

1. 'carry'라는 변수를 사용해 두 정수의 합이 10이 넘는지 안 넘는지 판단한다.

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
        public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode rCurrent = dummy;
        int carry = 0;

        while (l1 != null || l2 != null || carry != 0) {
            int sum = carry;
            if (l1 != null) {
                sum += l1.val;
                l1 = l1.next;
            }
            if (l2 != null) {
                sum += l2.val;
                l2 = l2.next;
            }
            carry = sum / 10;
            rCurrent.next = new ListNode(sum % 10);
            rCurrent = rCurrent.next;
        }

        return dummy.next;
    }
}

 

위의 코드를 보면 내가 처음 생각했던 코드보다 훨씬 간결하고 가독성이 좋다.

 

  • 먼저, 처음과 같이 dummy를 0으로 초기화해 임시 노드를 만들고, current를 더미를 가리키도록 한다. 그 다음 내가 언급했던 carry(올림수)를 0으로 초기화 한다.
  • int sum = carry : 이전 계산에서 두 정수의 합이 10이 넘었는지 확인하고, 넘었다면 다음 덧셈값에 이 carry(올린 자릿수)를 추가로 더해준다. 예를들어, 예를 들어보자.
    • 첫번째 while loop : sum은 0으로 초기화 되어있다. 이 sum에다가 각 연결 리스트의 head값인 2  + 5를 해 준다. 그럼 7이고, 7은 10을 넘지 않으므로 carry의 값은 7 / 10으로 0.7이지만, 자바에서의 int는 정수 뒤의 소수점을 무시하므로 첫번째 while에서는 carry의 값이 0이 된다. 그 다음, dummy node의 다음 노드값으로 결과값을 넣어준다. 7 % 10은 7 임으로 현재 결과노드는 이렇게 생겼다. 0 -> 7.
    • 두번째 while loop : sum은 다시 0으로 초기화 된다. 첫번째 while loop에서 10을 넘지 않았기 때문이다(== carry가 0임.). 이제 노드의 두번째 값들을 서로 더해준다. (4 + 6) 그러면 sum은 10이 되지만, carry 10/10에서 carry는 1이 된다. 여기서 1은 이제 다음 sum에서 올려줘야 할 값이다. 그 다음 10 % 10 = 0를 결과 노드에 다음 노드로 추가 해 준다.
    • 세번째 while loop : sum은 0이 아니라 1로 초기화 된다(이전 while loop에서 올림값). 이제 각 연결 리스트의 마지막 값들을 더해준다. (4+3) 하지만 여기에다가 우리가 10이 넘어 올라온 올림값 1을 더해 8로 만들어 준다. 이제 이 8을 마지막 결과 노드로 넣어주면 우리가 원하는 값을 얻을 수 있다.

이렇게 문제를 풀고 나니 느낀점 :  처음부터 코드를 다짜고짜 써 내려 갈 생각을 하지 말고, 문제를 좀 더 꼼꼼히 보고 종이와 펜을 준비해 어떠한 다른 방법이 있는지부터 천천히 살펴보자.

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

You can return the answer in any order.

Example 1:

Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].

Example 2:

Input: nums = [3,2,4], target = 6
Output: [1,2]

Example 3:

Input: nums = [3,3], target = 6
Output: [0,1]

Constraints:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

다시 말해, 이 문제에서 해결하고자 하는 문제는

주어진 배열에 대한 각 정수에 대하여 두개의 각기 다른 인덱스의 정수의 합이 target과 같도록 두개의 인덱스를 반환해라.

몇가지 방법이 있는데 내가 처음 생각난 방법은 Brute Force이다. 말 그대로 하나하나씩 일일히 짝을 찾아보는것이다. 예를들어, [2,7,11,15]의 배열이 있고, 타겟이 9일때, for문을 돌면서 비교한다. 예를들어, 배열의 첫번째 요소인 2부터 시작해서 이중 for문을 사용하여 그 다음 요소와 계속 비교한다. 그렇지만 이 문제에서는 정확한 두개의 인덱스를 반환해야하고, 배열은 정렬되어 있지 않기 때문에 수많은 탐색 시간이 소요된다. - O(n^2).

두번째 - 두개의 포인터를 이용.[해시테이블]

먼저 해시 테이블에 대해 알아야 한다. 해시 테이블이란 Key-Value pair로 매핑되는 가장 중요한 자료구조중 하나로써, 해시 함수를 이용한 삽입, 검색, 삭제에서 낮은 시간복잡도를 보인다[해시 테이블에 대해 잘 모른다면 유튜브나 구글링을 해보면 쉽게 이해 할 수 있다]. 그렇다면 위의 문제에서 해시 테이블을 어떻게 활용해야할까? [파이썬 에서는 해시맵이나 테이블 대신 딕셔너리 라고 부른다.] {} 기호로 표시한다.

먼저 리턴값을 생각해보자. 우리는 두개의 인덱스를 리스트형태로 반환해야 한다. 두개의 값을 더해 의도된 정수값이 나오는 방법을 생각해보자.

1. 리스트를 순회하며 타겟값에서 리스트의 정수값을 뺀다.
- 이유는 예를들어 타겟이 9이고 리스트의 0번째 정수가 2라면 9에서 2를 뺀 7을 리스트에서 찾아내면 그대로 반환하면 된다.

이해가 안 간다면 다시, 주어진 리스트 list = [1,5,3,12,9], 타겟을 10이라고 해보자. 이 리스트에서 가능한 조합은 1+9 이다. 리스트의 순서는 상관 없다고 했으므로 리턴되는 리스트 값은 [0, 4] 또는 [4,0]이여야 한다.

 

자 그렇다면 위의 내용을 코드로 어떻게 구현할까?

 

파이썬의 딕셔너리(aka HashTable)은 {} 이 기호로 표시해준다. 자료구조 수업에서는 자바를 이용했는데 해시맵을 import하고 객체를 만들때도 HashMap<Integer> map = new HashMap<>(); 처럼 직접 자료형을 선언해줬어야 했는데 이런거 보면 파이썬은 다른 프로그래밍 언어보다 훨씬 간결해보인다.

 

1. 딕셔너리 선언하기

numDictionary = {}

 

{}로 표현한 딕셔너리에 numDictionary라는 이름을 부여해줬다. 이제 우리의 딕셔너리는 이 이름으로 참조하면 된다.

 

2. 반복문

이제 반복문을 통해 리스트를 순회하면서 주어진 값을 찾아야 한다. 가독성이 쉽도록 리스트의 길이는 변수로 만들겠다.

반복문중에서 가장 많이 쓰이는  for문은 파이썬에서 이렇게 쓰인다.

n = len(list)

for i in range n :
	complement = target - nums[i]

 

여기서의 complement는 타겟값에서 리스트의 정수를 뺀것이다. 즉, 이 변수는 우리가 리스트에서 찾아야 할 값이다.

 

3. 조건문

파이썬의 여러가지 메서드중 편리한 built-in 메서드가 있는데, if a in b 메서드이다.

b안에 a가 있으면 true를 반환하고, 그렇지 않으면 false를 반환한다. 여기서의 a는 딕셔너리 안의 "키값"을 참조한다.  b의 자료형은 리스트, 딕셔너리, 튜플, 문자열(즉, 순서가 있는) 모두 가능하다.

 

이제 조건문을 통해 우리가 계산한 complement(보수, 즉 찾아야 할 값)이 리스트 안에 있는지 확인해보면 된다.

n = len(list)

for i in range n :
	complement = target - nums[i]
    if complement in numDictionary :
    	return [numMap[complement], i]
    numDictionary[nums[i]] = i #key - value 매핑
return []

 

 

if문을 살펴보자. 만약 딕셔너리 안에 보수가 있다면 현재 루프의 기준 인덱스 i와 그 보수값의 인덱스를 찾아 리턴하면 된다. 만약 딕셔너리 안에 찾으려는 보수값이 없다면, 딕셔너리 안에 키-값으로 매핑된 값을 넣어준다. 여기서의 Key는 리스트에서의 인덱스 i에 해당하는 값이고, value는 for문을 도는 기준값 i이다. 원하는 값이 없다면 비어있는 리스트를 리턴한다.

 

위에서 든 예시를 다시 코드와 함께 보자.

 

주어진 리스트 [2,7,11,15], 타겟은 9이고, 텅 빈 딕셔너리{} 가 있다. 리스트의 길이는 4이고, for문을 돈다면 0부터 3까지 순회할것이다.

 

  • 첫번째 루프 (i = 0)
    • complement = 9 - nums[0] = 7 이다.
    • if 7 in numDictionary : => 현재 딕셔너리는 비어있으므로 False를 반환하고 if문 밖으로 빠져 나간다.
    • numDictionary[nums[0]] = 0 => 딕셔너리 안에 키 2, value값 0을 매핑해준다. {2 : 0}
  • 두번째 루프 (i = 1)
  • complement = 9 - nums[1] = 2 이다.
  • if 2 in numDictionary : 현재 딕셔너리에 2 라는 키값이 존재하는지 확인한다. 근데 우리가 위에서 {2 : 0}의 쌍을 딕셔너리에 추가했다. 그렇다면 이 조건문은 true가 반환되어 if문 안의 리턴문이 실행될것이다.
  • 리턴 타입은 리스트로 지정되었기 때문에, return []형식으로 리스트를 반환한다. 첫번째 반환값은 numDictionary[2]이다. 이렇게 하면 딕셔너리에서 키값 2에 해당하는 value를 찾아 리턴할 것이다. 위에서 우리는 딕셔너리에 {2 : 0}을 매핑하였으므로 위의 구문에선 0이 리턴된다.
  • 그 다음 리턴값은 현재 순회중인 i값을 나타낸다. 그렇다면 차례로 [0, 1]이 리턴되고, 주어진 리스트 nums = [2,7,11,15]에서 0번째, 1번째 값 (2 + 7)은 9이므로 정답이다.

 

 

+ Recent posts