ArrayList 시간복잡도

|

ArrayList 시간복잡도

ArrayList의 메서드 get(), add(), remove()의 시간복잡도를 알아보자.

목차

  1. get(int index)
  2. add()
    • add(E element)
    • add(int index, E element)
  3. remove(int index)

1. get(int index)


public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

ArrayList는 Obect[]로 구현되어 있으므로 랜덤엑세스가 가능하다. 따라서 시간복잡도 : O(1)

2. add()

add()의 경우 배열의 마지막에 element를 추가하는 add(E element) 메서드와
특정 인덱스에 element를 추가하는 add(int index, E element) 메서드로 나뉜다.

2-1. add(int index)


public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}


메서드는 크게 두 부분으로 나뉜다. - 배열의 크기를 확인하는 부분 -> “ensureCapacityInternal(size + 1)” - 배열의 lastIndex에 element를 추가하는 부분 -> “elementData[size++] = e;”

배열의 크기를 확인하는 부분을 까보면
size >= length일 경우에 grow() 메서드가 실행됨을 알 수 있다.


private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}   

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    // size >= length이므로 grow() 메서드 호출
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

grow() 메서드는 배열을 복사하므로 이 작업의 시간복잡도는 배열의 원소의 갯수 n에 비례한다. 따라서 배열의 크기를 확인하는 부분의 시간복잡도는 O(n)이다.

배열에 lastIndex에 element를 추가하는 부분의 시간복잡도는 get()과 마찬가지로 O(1)이다.

즉 add(E element)는 시간복잡도 O(n)과 O(1)이 공존하는데 이를 계산하기 위해 분할상환분석을 사용한다.

분할상관분석에 의해 배열의 크기를 증가하는 작업(시간복잡도가 O(n))보다는 배열의 lastIndex에 element를 추가하는 작업이 훨씬 많으므로 이를 평균내면 O(1)의 시간복잡도를 가진다.

2-2. add(int index, E element)


public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

해당 index의 오른쪽에 존재하는 모든 element들의 index를 옮겨야 하므로 시간복잡도는 배열의 원소의 갯수 n에 비례한다. 따라서 시간복잡도는 O(n)이다.

3. remove(int index)


public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

해당 index의 오른쪽에 존재하는 모든 element들의 index를 옮겨야 하므로 시간복잡도는 배열의 원소의 갯수 n에 비례한다. 따라서 시간복잡도는 O(n)이다.

ref. https://stackoverflow.com/questions/45220908/why-arraylist-add-and-addint-index-e-complexity-is-amortized-constant-time

ref. https://ko.wikipedia.org/wiki/%EB%B6%84%ED%95%A0%EC%83%81%ED%99%98%EB%B6%84%EC%84%9D

Recursion (재귀)

|

목차

1. 정의
2. 재귀적인 접근
3. Recursion vs Iteration
4. 재귀 알고리즘 설계시 주의할 점
    - base case
    - 암시적 매개변수를 명시적 매개변수로 바꾸어라.

1. 정의

  • 자기 자신을 호출 하는 함수

public static void main(String[] args){
    recursive(); 
}

public static void recursive(){
    System.out.println("recursive");
    recursive(); // 자기 자신을 호출
}


2. 재귀적인 접근

  • 길이가 n인 문자열이 있다고 하자 .
    이 문자열의 길이를 구할 때 for문으로 구한다고 하면 이렇게 구할 수 있을 것이다.
    
    public static int length(String str){

        if(str == null || str.equals(""))
            return 0;

        int length = 0;
        for(int i=0; i<str.length(); i++) {
            length++;
        }
        return length;
    }

이번엔 for문 없이 재귀적으로 문자열의 길이를 구해보자.


    public static int lengthByRecursive(String str){

        if(str == null || str.equals(""))
            return 0;

        return 1+length(str.substring(1));
    }

3. Recursion vs Iteration

- 모든 재귀함수(Recursion)는 반복문(iteration)으로 변경가능하다.
- 그 역도 성립한다. 즉, 모든 반복문은 Recursion으로 표현 가능하다.
- 재귀함수는 복잡한 알고리즘을 단순하고 알기쉽게 표현하는 것을 가능하게 한다.
- 하지만 함수 호출에 따른 오버헤드가 있다(매개변수 전달, 액티베이션 프레임생성 등)

4. 재귀 알고리즘 설계시 주의할 점

  • base case
    • 1의 예제 실행 시 종료되지 않고 무한 호출 된다.
    • 이를 막기 위해 적어도 하나의 base case, 즉 순환되지 않고 종료되는 case가 있어야 함.
    • 모든 case는 결국 base case로 수렴해야 한다.

public static void main(String[] args){
    recursive(5); // 5번 호출
}

public static void recursive(int n){
    // base case
    if(n <= 0)
        return;

    System.out.println("recursive");
    recursive(n-1); // 자기 자신을 호출
}
  • 암시적 매개변수를 명시적 매개변수로 바꾸어라.
    • 순차 탐색

public int search(int[] data, int n, int target){
    for(int i=0; i<n; i++){
        if(data[i]==target)
            return i;
    }
    return -1;
}

이 함수의 미션은 data[0]에서 data[n-1] 사이에서 target을 검색하는 것이다.
하지만 검색 구간의 시작 인덱스 0은 보통 생략한다. 즉 암시적 매개변수다.
이렇게 암시적 매개변수로 함수(메서드)를 만들게 되면 재귀함수를 호출할 수 없으므로
아래와 같이 명시적으로 매개변수를 수정하여야 한다.


// 앞에서부터 순차적으로 target을 찾는 재귀함수
public int search(int[] data, int begin, int end, int target){
    if(begin>end)
        return -1;
    else if(target == data[begin]){
        return begin;
    }
    else
        return search(data,begin+1,end,target);

}

다운캐스팅 시 컴파일 에러가 발생하지 않는 이유

|

다운캐스팅 시 컴파일 에러가 발생하지 않는 이유

  • 다운 캐스팅이란?

상위 클래스에서 하위 클래스로 캐스팅 하는 것.


   public static void main(String[] args) {
        Object obj = new Object();
        String s = (String) obj; // down-casting
        Object o = (Object) new String("string"); // up-casting

    }

본격적인 설명에 들어가기 전에 클래스 다이어그램을 보자.

알다시피 Object 클래스는 자바에 존재하는 모든 객체들의 최상위 클래스다. 따라서 Object가 아닌 객체 -> Object(업 캐스팅) : true Object -> Object가 아닌 객체(다운 캐스팅) : ?

Object를 확장한 객체는 Object 객체가 가진 데이터(프로퍼티,메서드)보다 많은 걸 내포할 가능성이 있기 때문이다.

위 코드는 컴파일 시점에서는 문제가 없지만 코드를 실행하게 되면 Exception in thread “main” java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String runtime Exception이 발생한다.

미리 컴파일시점에 이런 문제를 발견하면 좋을텐데 왜 컴파일러는 이런 문제를 발견하지 못하는 걸까?

바로 객체의 동적 할당과 다형성 때문이다.


   public static void main(String[] args) {
        Object obj = new Object();
        String s = (String) obj; // down-casting (throw ClassCastException)

        Object strObj = new String("str"); // type is Object, but Concrete class is String.
        String str = (String) strObj; // down-casting (valid)
    }

자바는 new 연산자를 통해 객체를 동적 할당(런타임 시점에 할당) 하므로 컴파일 시점에서는 변수 obj, strObj는 Object 타입이라는 것만 알 수 있고 실제 할당되는 객체는 알 수 없다.

  • 컴파일 시점

// 컴파일 시점(실제 할당되는 객체는 알 수 없다)
Object obj = null;
String s = (String) obj;
Object strObj = null;
String str = (String) strObj;

-> String 타입은 Object 타입이므로 (다형성) 컴파일러 입장에서 obj와 strObj의 구현 클래스를 모르는 상황에서 캐스팅을 막을 수 없다.

  • 런타임 시점

// 런타임 시점(실제 객체가 할당된다)
Object obj = new Object();
String s = (String) obj; // down-casting (throw ClassCastException)
Object strObj = new String("str");
String str = (String) strObj; // down-casting (valid)

-> 1. obj : 구현 클래스(Object) -> String으로 캐스팅 하였으므로 ClassCastException이 발생. 2. strObj : 구현 클래스(String) -> String으로 캐스팅 하였으므로 캐스팅 성공.

자바 컬렉션 프레임워크(JCF)의 사용이 보편화된 이유 중에 하나도 JCF가 탄생하기 전에 Object 타입을 이용한 컨테이너를 만들어 사용할 때 발생하는

  1. element를 꺼낼때마다 일일이 캐스팅 해야 하는 번거로움
  2. 다운 캐스팅 문제발생 가능성이 존재 한다고 알고 있었는데 이번 기회를 통해 확실히 JCF의 소중함을 알게 되었다.

ref. https://stackoverflow.com/questions/14180861/downcasting-upcasting-error-at-compile-time-runtime
ref. https://www.geeksforgeeks.org/new-operator-java/
ref. https://www.geeksforgeeks.org/g-fact-46/
ref. https://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/garbage_collect.html#wp1085990

List.add()가 항상 return true인 이유

|

List.add()가 항상 return true인 이유

List.add()의 return type이 boolean인데 List 인터페이스의 구현 클래스인 ArrayList와 LinkedList의 return 값이 항상 true인 게 의문이 들었다.


ArrayList.java

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }



LinkedList.java

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

그래서 List의 superType인 Collection을 찾아봤더니

Ensures that this collection contains the specified element (optional operation). Returns true if this collection changed as a result of the call. (Returns false if this collection does not permit duplicates and already contains the specified element.) -java doc-

Collection에 중복이 허용되지 않거나, 특정 element가 존재하는 경우 return false라고 한다.

List 타입은 중복을 허용하므로 ArrayList와 LinkedList가 항상 true를 리턴 할 수 밖에 없구나..

ref. https://docs.oracle.com/javase/7/docs/api/

Java 8 in Action(2)

|

Java 8 in Action(Chapter 3)

이 장의 내용

  1. 람다란 무엇인가?
  2. 어디에, 어떻게 람다를 사용하는가?
  3. 실행 어라운드 패턴
  4. 함수형 인터페이스, 형식 추론
  5. 메서드 레퍼런스
  6. 람다 만들기

  7. 람다란 무엇인가?
    • 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름은 없지만 1) 파라미터 리스트 2) 바디 3) 리턴 타입 4) 발생할 수 있는 예외 리스트는 가질 수 있다.

// 람다 표현식 기본 문법
1. (parameters) -> expression
2. (parameters) -> {statements;}


// 람다 표현식 1
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
--------------------    ----------------------------------------
    파라미터 리스트          바디(return 함축되어 있으므로 리턴 타입은 int)

// 람다 표현식 2
(int x, int y) -> { System.out.println("Result:");
                    System.out.println(x+y); }

--------------    ---------------------------------------------
 파라미터 리스트                   바디(리턴 타입은 void)

// 람다 표현식 3
  ()        ->     42
-------           ----
파라미터 x          리턴 타입은 int
람다의 특징
    1) 익명
        - 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다.

    2) 함수
        - 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.

    3) 전달
        - 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.

    4) 간결성
        - 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
  1. 어디에, 어떻게 람다를 사용하는가?
    • 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.

    • 함수형 인터페이스

      • 정확히 하나의 추상 메서드를 지정하는 인터페이스. ex) Comparator, Runnable …
      • @FunctionalInterface 어노테이션으로 명시적으로 표기한다. 해당 어노테이션을 선언한 인터페이스에 추상 메서드가 한 개 이상이라면 컴파일 에러가 발생한다.

ref. http://www.hanbit.co.kr/store/books/look.php?p_code=B1999551123