언제 List와 LinkedList를 사용해야합니까?


답변:


107

편집하다

이 답변에 대한 의견을 읽으십시오. 사람들은 내가 적절한 테스트를하지 않았다고 주장합니다. 나는 이것이 받아 들여서는 안된다는 데 동의합니다. 배우면서 몇 가지 테스트를 수행하고 공유하는 느낌이 들었습니다.

원래 답변 ...

흥미로운 결과를 찾았습니다.

// Temporary class to show the example
class Temp
{
    public decimal A, B, C, D;

    public Temp(decimal a, decimal b, decimal c, decimal d)
    {
        A = a;            B = b;            C = c;            D = d;
    }
}

연결된 목록 (3.9 초)

        LinkedList<Temp> list = new LinkedList<Temp>();

        for (var i = 0; i < 12345678; i++)
        {
            var a = new Temp(i, i, i, i);
            list.AddLast(a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

목록 (2.4 초)

        List<Temp> list = new List<Temp>(); // 2.4 seconds

        for (var i = 0; i < 12345678; i++)
        {
            var a = new Temp(i, i, i, i);
            list.Add(a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

본질적으로 데이터에만 액세스하더라도 훨씬 느립니다 !! 나는 linkedList를 사용하지 않는다고 말한다.




다음은 많은 인서트를 수행하는 또 다른 비교입니다 (목록 중간에 항목을 삽입 할 계획입니다)

연결 목록 (51 초)

        LinkedList<Temp> list = new LinkedList<Temp>();

        for (var i = 0; i < 123456; i++)
        {
            var a = new Temp(i, i, i, i);

            list.AddLast(a);
            var curNode = list.First;

            for (var k = 0; k < i/2; k++) // In order to insert a node at the middle of the list we need to find it
                curNode = curNode.Next;

            list.AddAfter(curNode, a); // Insert it after
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

목록 (7.26 초)

        List<Temp> list = new List<Temp>();

        for (var i = 0; i < 123456; i++)
        {
            var a = new Temp(i, i, i, i);

            list.Insert(i / 2, a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

삽입 할 위치에 대한 참조가있는 링크 된 목록 (.04 초)

        list.AddLast(new Temp(1,1,1,1));
        var referenceNode = list.First;

        for (var i = 0; i < 123456; i++)
        {
            var a = new Temp(i, i, i, i);

            list.AddLast(a);
            list.AddBefore(referenceNode, a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

그래서 단지 당신이 여러 항목을 삽입 계획하고있는 경우 어딘가에는 다음 링크 된 목록을 사용하여 항목을 삽입 할 계획 곳의 레퍼런스를 가지고있다. 많은 항목을 삽입해야하기 때문에 삽입하려는 위치를 검색하면 시간이 걸리기 때문에 더 빠르지 않습니다.


99
List보다 LinkedList의 이점이 하나 있습니다 (.net에만 해당). List는 내부 배열에 의해 지원되므로 하나의 연속 블록으로 할당됩니다. 할당 된 블록의 크기가 85000 바이트를 초과하면 호환되지 않는 생성 인 Large Object Heap에 할당됩니다. 크기에 따라 약간의 메모리 누수 형태 인 힙 조각화가 발생할 수 있습니다.
JerKimball

35
당신이 많이 선행하거나 (마지막 예에서와 같이) 첫 번째 항목을 삭제하는 경우, 검색 또는 이동 / 복사가 없기 때문에 링크 된 목록이 거의 항상 훨씬 빠릅니다. List는 새 항목을 수용하기 위해 모든 것을 한 자리 위로 이동하여 O (N) 연산을 앞에 추가해야합니다.
cHao

6
list.AddLast(a);마지막 두 LinkedList 예제에서 왜 루프 가 발생합니까? list.AddLast(new Temp(1,1,1,1));마지막 LinkedList 옆에있는 것처럼 루프 전에 한 번 수행 하지만 루프 자체에 Temp 객체를 두 배 더 추가하는 것처럼 보입니다. (그리고 내가 LinkedList에서 두 배나 많은 테스트 앱으로 나 자신을 다시 확인할 때 .)
ruffin

7
나는이 대답을 downvoted. 1) I say never use a linkedList.나중의 게시물에서 알 수 있듯이 일반적인 조언 에는 결함이 있습니다. 편집하고 싶을 수도 있습니다. 2) 타이밍은 무엇입니까? 한 번에 인스턴스화, 추가 및 열거? 대부분의 인스턴스화와 열거는 ppl이 걱정하는 것이 아니며 한 단계입니다. 특히 인서트와 추가 타이밍을 맞추는 것이 더 좋은 아이디어가 될 것입니다. 3) 가장 중요한 것은 링크리스트에 필요한 것 이상을 추가하는 것입니다. 이것은 잘못된 비교입니다. linkedlist에 대한 잘못된 아이디어가 퍼졌습니다.
nawfal

47
죄송하지만 이 답변은 정말 나쁩니다. 이 답변을 듣지 마십시오. 요컨대, 배열 지원 목록 구현이 각 삽입에서 배열의 크기를 조정하기에 충분히 바보라고 생각하는 것은 완전히 결함이 있습니다. 링크 된 목록은 순회 할 때와 양쪽 끝을 삽입 할 때 배열 기반 목록보다 자연스럽게 느립니다. 배열 지원 목록은 버퍼를 사용하지만 (양방향으로) 분명히 객체를 만들면되기 때문입니다. (불완전한) 벤치 마크는 정확하게 그 사실을 나타냅니다. 답변은 링크 된 목록이 바람직한 경우를 완전히 확인하지 못합니다!
mafu

277

대부분의 경우 List<T>더 유용합니다. LinkedList<T>목록 중간에 항목을 추가 / 제거 할 때 비용이 적게 드는 반면 목록 끝에List<T> 값을 추가 / 제거 할 수 있습니다 .

LinkedList<T>순차 또는 역순으로 순차 데이터에 액세스하는 경우에만 가장 효율적입니다. 랜덤 액세스는 매번 체인을 걸어야하기 때문에 상대적으로 비용이 많이 듭니다 (따라서 인덱서가없는 이유). 그러나 a List<T>는 본질적으로 배열 (래퍼 포함)이므로 임의 액세스가 좋습니다.

List<T>또한 지원 방법을 많이 제공합니다 - Find, ToArray등; 그러나 LinkedList<T>확장 방법을 통해 .NET 3.5 / C # 3.0 에서도 사용할 수 있으므로 그다지 중요하지 않습니다.


4
내가 생각한 적이없는 List <> vs. LinkedList <>의 한 가지 장점은 마이크로 프로세서가 메모리 캐싱을 구현하는 방법에 관한 것입니다. 비록 그것을 완전히 이해하지는 못하지만,이 블로그 기사의 작성자는 "참조의 지역성"에 대해 많이 이야기합니다 . 적어도 링크 된리스트가 메모리에서 약간 조각난 경우에는 링크 된리스트를 순회하는 것보다 배열을 순회하는 것이 훨씬 빠릅니다. . kjellkod.wordpress.com/2012/02/25/…
RenniePet

@RenniePet List는 동적 배열로 구현되며 배열은 인접한 메모리 블록입니다.
Casey

2
List는 동적 배열이므로 미리 알고 있으면 생성자에서 List의 용량을 지정하는 것이 좋은 경우가 있습니다.
카딘 리 JH

all, array, List <T> 및 LinkedList <T>의 C # 구현이 하나의 매우 중요한 경우에 다소 차선책 일 수 있습니까? 매우 큰 목록, 추가 (AddLast) 및 순차 순회 (한 방향으로) 완전히 괜찮습니다 : 연속 블록을 얻기 위해 배열 크기 조정을 원하지 않습니다 (각 배열, 20GB 배열까지 보장됩니까?). 크기를 미리 알지 못하지만 블록 크기를 미리 추측 할 수 있습니다 (예 : 100) 미리 예약 할 MB. 이것은 좋은 구현이 될 것입니다. 아니면 배열 / 목록과 비슷하며 요점을 놓쳤습니까?
Philm

1
@Philm 그것은 선택한 블록 전략에 대해 자신의 심을 쓰는 시나리오의 종류입니다. List<T>T[](모두 한 슬래브) 너무 땅딸막 한 것에 대해 실패합니다 LinkedList<T>(요소마다 슬래브) 너무 세부적인 것에 대해 통곡 할 것이다.
Marc Gravell

212

연결된 목록을 목록으로 생각하는 것은 약간 오해의 소지가 있습니다. 체인과 비슷합니다. 실제로 .NET에서는 LinkedList<T>조차 구현하지 않습니다 IList<T>. 비록 링크 된리스트에는 인덱스의 실제 개념이 없지만, 비록 존재하는 것처럼 보일 수 있습니다. 클래스에 제공된 메소드 중 어느 것도 인덱스를 허용하지 않습니다.

연결된 목록은 단독으로 연결되거나 이중으로 연결될 수 있습니다. 이것은 체인의 각 요소가 다음 요소에만 링크되는지 (단일 링크 됨) 또는 이전 / 다음 요소 모두에 링크가 있는지 (두 개 링크 됨)를 나타냅니다. LinkedList<T>이중 연결되어 있습니다.

내부적 List<T>으로 배열이 지원합니다. 이것은 메모리에서 매우 컴팩트 한 표현을 제공합니다. 반대로, LinkedList<T>연속 요소 사이의 양방향 링크를 저장하기위한 추가 메모리가 필요합니다. 따라서 a의 메모리 풋 프린트 LinkedList<T>는 일반적으로 메모리 용량 보다 더 클 것입니다 List<T>( List<T>추가 작업 중 성능을 향상시키기 위해 사용되지 않는 내부 배열 요소를 가질 수 있는 경고가 있음 ).

성능 특성도 다릅니다.

추가

  • LinkedList<T>.AddLast(item) 일정한 시간
  • List<T>.Add(item) 상각 일정 시간, 선형 최악의 경우

접두사

  • LinkedList<T>.AddFirst(item) 일정한 시간
  • List<T>.Insert(0, item) 선형 시간

삽입

  • LinkedList<T>.AddBefore(node, item) 일정한 시간
  • LinkedList<T>.AddAfter(node, item) 일정한 시간
  • List<T>.Insert(index, item) 선형 시간

제거

  • LinkedList<T>.Remove(item) 선형 시간
  • LinkedList<T>.Remove(node) 일정한 시간
  • List<T>.Remove(item) 선형 시간
  • List<T>.RemoveAt(index) 선형 시간

카운트

  • LinkedList<T>.Count 일정한 시간
  • List<T>.Count 일정한 시간

포함

  • LinkedList<T>.Contains(item) 선형 시간
  • List<T>.Contains(item) 선형 시간

명확한

  • LinkedList<T>.Clear() 선형 시간
  • List<T>.Clear() 선형 시간

보다시피, 그것들은 대부분 동등합니다. 실제로 API는 사용하기 LinkedList<T>가 더 번거로우 며 내부 요구 사항에 대한 세부 정보가 코드에 유출됩니다.

그러나 목록 내에서 많은 삽입 / 제거를 수행해야하는 경우 일정한 시간을 제공합니다. List<T>삽입 / 제거 후 목록의 추가 항목을 뒤섞어 야하므로 선형 시간을 제공합니다.


2
count linkedlist가 일정합니까? 나는 그것이 선형 일 것이라고 생각 했습니까?
Iain Ballard

10
@Iain, 카운트는 두 목록 클래스에 캐시됩니다.
Drew Noakes

3
"List <T> .Add (item) logarithmic time"이라고 썼지 만, 목록 용량이 새 항목을 저장할 수 있으면 실제로는 "Constant"이고 목록에 공간이 충분하지 않고 "Linear"이면 재 할당됩니다.
aStranger

물론 @aStranger입니다. 내가 위에서 생각한 것을 잘 모르겠다. 아마도 상각 된 정상적인 사건 시간은 대수 일 것이다. 실제로 상각 된 시간은 일정합니다. 나는 간단한 비교를 목표로 최고의 / 최악의 운영 사례를 얻지 못했습니다. 그러나 추가 작업 이이 세부 정보를 제공 할만 큼 중요하다고 생각합니다. 답변을 편집합니다. 감사.
Drew Noakes

1
@Philm, 새로운 질문을 시작해야 할 것입니다.이 데이터 구조를 한 번 빌드 한 후에 어떻게 사용할지 말하지 않지만 백만 행을 이야기하는 경우 일종의 하이브리드 (링크 된 목록) 힙 조각화를 줄이고 메모리 오버 헤드를 줄이며 LOH에서 하나의 거대한 객체를 피하십시오.
Drew Noakes

118

연결된 목록은 목록 구성원을 매우 빠르게 삽입하거나 삭제합니다. 링크 된 목록의 각 멤버에는 목록의 다음 멤버에 대한 포인터가 포함되어 i 위치에 멤버를 삽입합니다.

  • i-1 멤버의 포인터를 업데이트하여 새 멤버를 가리 킵니다.
  • 새 멤버의 포인터를 멤버 i를 가리 키도록 설정하십시오.

연결된 목록의 단점은 임의 액세스가 불가능하다는 것입니다. 멤버에 액세스하려면 원하는 멤버를 찾을 때까지 목록을 탐색해야합니다.


6
링크 된 목록에는 이전 및 다음 노드를 참조하는 LinkedListNode를 통해 위에 저장된 항목 당 오버 헤드가 있다고 덧붙입니다. 그 결과는 배열 기반 목록과 달리 목록을 저장하는 데 연속적인 메모리 블록이 아닙니다.
paulecoyote

3
연속적인 메모리 블록이 일반적으로 바람직하지 않습니까?
Jonathan Allen

7
그렇습니다. 연속 액세스는 랜덤 액세스 성능과 메모리 소비에 선호되지만 정기적으로 크기를 변경해야하는 컬렉션의 경우 일반적으로 Array와 같은 구조를 새 위치로 복사해야하지만 연결된 목록은 메모리를 관리하기 만하면됩니다. 새로 삽입 / 삭제 된 노드
jpierson

6
매우 큰 배열 또는 목록 (목록이 배열을 감싸는 것)으로 작업해야한다면 시스템에 사용 가능한 메모리가 많더라도 메모리 문제가 발생하기 시작합니다. 이 목록은 기본 배열에 새 공간을 할당 할 때 배가 전략을 사용합니다. 따라서 가득 찬 1000000 elemnt 배열은 2000000 요소를 가진 새로운 배열로 복사됩니다. 이 새로운 어레이는이를 수용 할 수있을만큼 큰 연속 메모리 공간에 작성해야합니다.
앤드류

1
나는 내가 한 모든 것이 추가하고 제거하고 하나씩 반복하는 특별한 경우를 가졌습니다. 여기에 연결된 목록이 일반 목록보다 훨씬 우수했습니다.
Peter

26

내 이전 답변이 충분히 정확하지 않았습니다. 정말 끔찍했습니다 : D 그러나 지금은 훨씬 더 유용하고 정확한 답변을 게시 할 수 있습니다.


추가 테스트를했습니다. https://github.com/ukushu/DataStructuresTestsAndOther.git 링크를 통해 소스를 찾아 환경에서 다시 확인할 수 있습니다.

짧은 결과 :

  • 배열은 다음을 사용해야합니다.

    • 가능한 자주. 동일한 양의 정보를 얻기 위해 빠르며 RAM 범위가 가장 작습니다.
    • 필요한 정확한 세포 수를 알고 있다면
    • 배열 <85000b에 저장된 데이터 (정수 데이터의 경우 85000/32 = 2656 요소)
    • 높은 랜덤 액세스 속도가 필요한 경우
  • 목록을 사용해야합니다.

    • 목록 끝에 셀을 추가해야하는 경우 (종종)
    • 목록의 시작 / 중간에 셀을 추가해야하는 경우
    • 배열 <85000b에 저장된 데이터 (정수 데이터의 경우 85000/32 = 2656 요소)
    • 높은 랜덤 액세스 속도가 필요한 경우
  • LinkedList는 다음을 사용해야합니다.

    • 목록의 시작 / 중간 / 끝에 셀을 추가해야하는 경우 (종종)
    • 순차적 액세스 만 필요한 경우 (앞으로 / 뒤로)
    • 큰 항목을 저장해야하지만 항목 수가 적은 경우
    • 링크에 추가 메모리를 사용하므로 대량의 항목에는 사용하지 않는 것이 좋습니다.

자세한 내용은:

введите сюда описание изображения 알고 싶은 것 :

  1. LinkedList<T>internally는 .NET의 List가 아닙니다. 심지어 구현하지도 않습니다 IList<T>. 이것이 인덱스와 관련된 인덱스와 메소드가없는 이유입니다.

  2. LinkedList<T>노드 포인터 기반 컬렉션입니다. .NET에서는 이중 링크 구현입니다. 이는 이전 / 다음 요소가 현재 요소에 연결되어 있음을 의미합니다. 또한 데이터가 조각화되어 다른 RAM의 다른 위치에 다른 목록 개체를 배치 할 수 있습니다. 또한 LinkedList<T>for List<T>또는 Array 보다 더 많은 메모리가 사용됩니다 .

  3. List<T>.Net에서 Java의 대안입니다 ArrayList<T>. 이것은 이것이 배열 래퍼임을 의미합니다. 따라서 하나의 연속 된 데이터 블록으로 메모리에 할당됩니다. 할당 된 데이터 크기가 85000 바이트를 초과하면 Large Object Heap으로 이동합니다. 크기에 따라 힙 조각화 (가벼운 형태의 메모리 누수)가 발생할 수 있습니다. 그러나 크기가 85000 바이트 미만인 경우 메모리에서 매우 작고 빠른 액세스 표현을 제공합니다.

  4. 랜덤 액세스 성능과 메모리 소비에는 단일 연속 블록이 선호되지만 정기적으로 크기를 변경해야하는 컬렉션의 경우 일반적으로 Array와 같은 구조를 새 위치로 복사해야하지만 연결된 목록은 새로 삽입 된 메모리 만 관리하면됩니다. / 삭제 된 노드.


1
질문 : "배열 <또는> 85.000 바이트에 저장된 데이터"를 사용하면 배열 / 목록 당 데이터를 의미합니다. ELEMENT? 전체 어레이의 데이터 크기를 의미한다는 것을 이해할 수 있습니다.
Philm

메모리에 순차적으로 위치한 배열 요소. 따라서 배열 당. 나는 테이블에 실수가 있다는 것을 알고 나중에 고칠 것이다 :) (희망 ....)
Andrew

목록이 삽입 속도가 느리면 목록에 많은 처리 시간 (많은 삽입 / 삭제)이 있으면 삭제 된 공간이 차지하는 메모리가 채워지고 그렇다면 그렇다면 "재"삽입이 더 빨라 집니까?
Rob

18

List와 LinkedList의 차이점은 기본 구현에 있습니다. List는 배열 기반 컬렉션 (ArrayList)입니다. LinkedList는 노드 포인터 기반 콜렉션 (LinkedListNode)입니다. API 레벨 사용법에서 ICollection, IEnumerable 등과 같은 동일한 인터페이스 세트를 구현하므로 둘 다 거의 동일합니다.

중요한 차이점은 성능이 중요합니다. 예를 들어, "INSERT"작업이 많은 목록을 구현하는 경우 LinkedList가 List보다 성능이 우수합니다. LinkedList는 O (1) 시간에이를 수행 할 수 있지만 List는 기본 배열의 크기를 확장해야 할 수도 있습니다. 자세한 정보 / 세부 사항은 LinkedList와 배열 데이터 구조의 알고리즘 차이를 읽으십시오. http://en.wikipedia.org/wiki/Linked_list배열

이 도움을 바랍니다.


4
List <T>는 ArrayList 기반이 아니라 배열 (T []) 기반입니다. 다시 삽입 : 배열 크기 조정이 문제가되지 않습니다 (두 배의 알고리즘은 대부분의 경우이 작업을 수행 할 필요가 없음을 의미합니다). 문제는 기존의 모든 데이터를 먼저 차단해야한다는 것입니다. 시각.
Marc Gravell

2
'두배 알고리즘'인
@Marc

2
내 요점은 고통을 일으키는 것은 크기 조정이 아니라는 것입니다. 최악의 경우, 매번 첫 번째 (0) 요소를 추가하는 경우, 블리트는 매번 모든 것을 움직여야합니다.
Marc Gravell

@IlyaRyzhenkov-당신 Add은 항상 기존 배열의 끝에 있는 경우에 대해 생각하고 있습니다. ListO (1)이 아니더라도 "충분히"좋습니다. 끝 Add아닌 많은 것들이 필요한 경우 심각한 문제가 발생합니다 . Marc는 크기를 조정해야 할 때뿐만 아니라 삽입 할 때마다 기존 데이터 를 이동 해야하는 비용이보다 실질적인 비용이라고 지적합니다 . List
ToolmakerSteve

문제는 이론적 Big O 표기법이 전체 이야기를 말하지 않는다는 것입니다. 모든 사람이 관심을 갖는 컴퓨터 과학에서는 현실 세계에서 이것보다 훨씬 더 걱정해야 할 것이 있습니다.
MattE

11

배열에 대한 링크 된 목록의 주요 장점은 링크가 항목을 효율적으로 재 배열 할 수있는 기능을 제공한다는 것입니다. 세지윅, p. 91


1
IMO가 답이되어야합니다. LinkedList는 보장 된 주문이 중요 할 때 사용됩니다.
RBaarda

1
@RBaarda : 동의하지 않습니다. 그것은 우리가 말하는 수준에 달려 있습니다. 알고리즘 수준은 기계 구현 수준과 다릅니다. 속도를 고려하려면 후자가 필요합니다. 지적한 바와 같이, 어레이는 제한적인 메모리의 "하나의 청크"로 구현되는데, 이는 특히 매우 큰 어레이에서 크기 조정 및 메모리 재구성으로 이어질 수 있기 때문이다. 잠시 동안, 특별한 데이터 구조, 배열의 연결된 목록은 선형 채우기 속도를 제어하고 매우 큰 데이터 구조에 액세스하는 것을 더 잘 제어하는 ​​아이디어가 될 것입니다.
Philm

1
@Philm-귀하의 의견을 찬성했지만 귀하가 다른 요구 사항을 설명하고 있음을 지적하고 싶습니다. 대답은 연결된 목록이 많은 항목 을 다시 정렬 하는 알고리즘에 대한 성능 이점이 있다는 것입니다 . 이를 감안할 때 RBaarda의 의견은 주어진 순서 (정렬 기준)를 지속적으로 유지하면서 항목을 추가 / 삭제해야한다고 말합니다. "선형 충전"만이 아닙니다. 이 경우 인덱스가 쓸모 없기 때문에 목록이 손실됩니다 (꼬리 끝에 요소를 추가 할 때마다 변경).
ToolmakerSteve

4

LinkedList를 사용하는 일반적인 상황은 다음과 같습니다.

크기가 큰 문자열 목록 (예 : 100,000)에서 많은 특정 문자열을 제거한다고 가정합니다. 제거 할 문자열은 HashSet dic에서 조회 할 수 있으며 문자열 목록에는 제거 할 문자열이 30,000-60,000 개 사이 인 것으로 생각됩니다.

그렇다면 100,000 개의 문자열을 저장하는 가장 좋은 유형의 목록은 무엇입니까? 답은 LinkedList입니다. 그것들이 ArrayList에 저장되어 있으면 그것을 반복하고 일치하는 문자열을 제거하는 데 최대 수십억 개의 연산을 취하는 반면 반복자와 remove () 메소드를 사용하면 약 10 만 개의 연산이 필요합니다.

LinkedList<String> strings = readStrings();
HashSet<String> dic = readDic();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
    String string = iterator.next();
    if (dic.contains(string))
    iterator.remove();
}

6
당신은 간단하게 사용할 수 있습니다 RemoveAll에서 항목을 제거 List항목이 여러 이동하지 않고, 사용하거나 Where두 번째 목록을 만들 수 LINQ에서. LinkedList그러나 here를 사용하면 다른 유형의 컬렉션보다 훨씬 더 많은 메모리를 소비 하게 되며 메모리 지역성이 손실되면 반복하는 데 눈에 띄게 느려서 a보다 약간 나빠집니다 List.
Servy

@Servy, @Tom의 답변은 Java를 사용합니다. RemoveAllJava에 동등한 것이 있는지 확실하지 않습니다 .
Arturo Torres Sánchez

3
@ ArturoTorresSánchez 글쎄,이 질문은 .NET에 관한 것이므로 구체적으로 대답을 덜 적절하게 만듭니다.
Servy

@Servy, 처음부터 언급 했어야합니다.
Arturo Torres Sánchez

RemoveAll사용할 수없는 경우 ListTom의 루프처럼 보이는 "압축"알고리즘을 수행 할 수 있지만 두 개의 인덱스와 목록의 내부 배열에서 한 번에 하나씩 유지되도록 항목을 이동해야합니다. 효율성은 O (n)이며에 대한 Tom의 알고리즘과 동일합니다 LinkedList. 두 버전 모두에서 문자열에 대한 HashSet 키를 계산하는 시간이 지배적입니다. 이것은 언제 사용해야하는 좋은 예가 아닙니다 LinkedList.
ToolmakerSteve

2

기본 제공 색인 액세스, 정렬 (및 이진 검색 후) 및 "ToArray ()"메소드가 필요한 경우 List를 사용해야합니다.


2

기본적 List<>으로 .NET의 배열배열 의 래퍼 입니다. A LinkedList<> 는 연결된 목록 입니다. 따라서 문제는 배열과 연결 목록의 차이점과 연결 목록 대신 배열을 사용해야하는 시점에 관한 것입니다. 아마도 당신의 결정에있어서 가장 중요한 두 가지 요소는 다음과 같습니다.

  • 삽입 / 제거가 컬렉션의 마지막 요소에 있지 않는 한 연결된 목록은 삽입 / 제거 성능이 훨씬 뛰어납니다. 배열이 삽입 / 제거 지점 다음에 나오는 나머지 모든 요소를 ​​이동해야하기 때문입니다. 그러나 삽입 / 제거가 목록의 끝에 있으면이 이동이 필요하지 않습니다 (용량을 초과하는 경우 어레이의 크기를 조정해야 할 수도 있음).
  • 어레이는 액세스 기능이 훨씬 뛰어납니다. 배열은 일정한 시간에 직접 색인을 생성 할 수 있습니다. 연결된 목록은 순회해야합니다 (선형 시간).

1

이것은 몇 가지 잘못된 측정을 수정하는 Tono Nam 의 승인 된 답변 에서 채택되었습니다 .

시험:

static void Main()
{
    LinkedListPerformance.AddFirst_List(); // 12028 ms
    LinkedListPerformance.AddFirst_LinkedList(); // 33 ms

    LinkedListPerformance.AddLast_List(); // 33 ms
    LinkedListPerformance.AddLast_LinkedList(); // 32 ms

    LinkedListPerformance.Enumerate_List(); // 1.08 ms
    LinkedListPerformance.Enumerate_LinkedList(); // 3.4 ms

    //I tried below as fun exercise - not very meaningful, see code
    //sort of equivalent to insertion when having the reference to middle node

    LinkedListPerformance.AddMiddle_List(); // 5724 ms
    LinkedListPerformance.AddMiddle_LinkedList1(); // 36 ms
    LinkedListPerformance.AddMiddle_LinkedList2(); // 32 ms
    LinkedListPerformance.AddMiddle_LinkedList3(); // 454 ms

    Environment.Exit(-1);
}

그리고 코드 :

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace stackoverflow
{
    static class LinkedListPerformance
    {
        class Temp
        {
            public decimal A, B, C, D;

            public Temp(decimal a, decimal b, decimal c, decimal d)
            {
                A = a; B = b; C = c; D = d;
            }
        }



        static readonly int start = 0;
        static readonly int end = 123456;
        static readonly IEnumerable<Temp> query = Enumerable.Range(start, end - start).Select(temp);

        static Temp temp(int i)
        {
            return new Temp(i, i, i, i);
        }

        static void StopAndPrint(this Stopwatch watch)
        {
            watch.Stop();
            Console.WriteLine(watch.Elapsed.TotalMilliseconds);
        }

        public static void AddFirst_List()
        {
            var list = new List<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
                list.Insert(0, temp(i));

            watch.StopAndPrint();
        }

        public static void AddFirst_LinkedList()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (int i = start; i < end; i++)
                list.AddFirst(temp(i));

            watch.StopAndPrint();
        }

        public static void AddLast_List()
        {
            var list = new List<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
                list.Add(temp(i));

            watch.StopAndPrint();
        }

        public static void AddLast_LinkedList()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (int i = start; i < end; i++)
                list.AddLast(temp(i));

            watch.StopAndPrint();
        }

        public static void Enumerate_List()
        {
            var list = new List<Temp>(query);
            var watch = Stopwatch.StartNew();

            foreach (var item in list)
            {

            }

            watch.StopAndPrint();
        }

        public static void Enumerate_LinkedList()
        {
            var list = new LinkedList<Temp>(query);
            var watch = Stopwatch.StartNew();

            foreach (var item in list)
            {

            }

            watch.StopAndPrint();
        }

        //for the fun of it, I tried to time inserting to the middle of 
        //linked list - this is by no means a realistic scenario! or may be 
        //these make sense if you assume you have the reference to middle node

        //insertion to the middle of list
        public static void AddMiddle_List()
        {
            var list = new List<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
                list.Insert(list.Count / 2, temp(i));

            watch.StopAndPrint();
        }

        //insertion in linked list in such a fashion that 
        //it has the same effect as inserting into the middle of list
        public static void AddMiddle_LinkedList1()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            LinkedListNode<Temp> evenNode = null, oddNode = null;
            for (int i = start; i < end; i++)
            {
                if (list.Count == 0)
                    oddNode = evenNode = list.AddLast(temp(i));
                else
                    if (list.Count % 2 == 1)
                        oddNode = list.AddBefore(evenNode, temp(i));
                    else
                        evenNode = list.AddAfter(oddNode, temp(i));
            }

            watch.StopAndPrint();
        }

        //another hacky way
        public static void AddMiddle_LinkedList2()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start + 1; i < end; i += 2)
                list.AddLast(temp(i));
            for (int i = end - 2; i >= 0; i -= 2)
                list.AddLast(temp(i));

            watch.StopAndPrint();
        }

        //OP's original more sensible approach, but I tried to filter out
        //the intermediate iteration cost in finding the middle node.
        public static void AddMiddle_LinkedList3()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
            {
                if (list.Count == 0)
                    list.AddLast(temp(i));
                else
                {
                    watch.Stop();
                    var curNode = list.First;
                    for (var j = 0; j < list.Count / 2; j++)
                        curNode = curNode.Next;
                    watch.Start();

                    list.AddBefore(curNode, temp(i));
                }
            }

            watch.StopAndPrint();
        }
    }
}

다른 사람들이 여기에 문서화 된 이론적 성능에 따른 결과를 볼 수 있습니다. 매우 명확합니다- LinkedList<T>삽입시 큰 시간을 얻습니다. 나는 목록의 중간에서 제거를 테스트하지 않았지만 결과는 동일해야합니다. 물론 List<T>O (1) 랜덤 액세스와 같이 더 나은 성능을 발휘하는 다른 영역이 있습니다.


0

LinkedList<>때 사용

  1. 플러드 게이트를 통해 얼마나 많은 물체가 들어오는 지 모릅니다. 예를 들면 다음과 같습니다 Token Stream.
  2. 끝 부분에서만 삭제 / 삽입하고 싶을 때.

다른 모든 것에는을 사용하는 것이 좋습니다 List<>.


6
포인트 2가 왜 의미가 있는지 모르겠습니다. 링크 된 목록은 전체 목록에서 많은 삽입 / 삭제를 수행 할 때 유용합니다.
Drew Noakes

LinkedLists가 인덱스를 기반으로하지 않기 때문에 O (n) 페널티가 발생하는 삽입 또는 삭제를 위해 전체 목록을 스캔해야합니다. 반면 List <>는 Array resizing으로 어려움을 겪지 만 LinkedList와 비교할 때 여전히 IMO가 더 나은 옵션입니다.
Antony Thomas

1
LinkedListNode<T>코드 에서 객체를 추적하는 경우 삽입 / 삭제를 위해 목록을 스캔 할 필요가 없습니다 . 그렇게 할 수 있다면 List<T>, 특히 삽입 / 제거가 빈번한 매우 긴 목록의 경우를 사용하는 것보다 훨씬 낫습니다 .
Drew Noakes

해시 테이블을 의미합니까? 이 경우 모든 컴퓨터 프로그래머가 문제 영역을 기반으로 선택해야하는 일반적인 시공간 상충 관계가 될 것입니다.) 그러나 그렇습니다.
Antony Thomas

1
@AntonyThomas-아니요, 그는 요소대한 참조를 전달하는 대신 노드대한 참조를 전달하는 것을 의미 합니다 . 모든 요소element 인 경우 검색해야하기 때문에 List 및 LinkedList 모두 성능이 저하됩니다. "목록을 사용하면 색인을 전달할 수 있습니다"라고 생각하면 목록 중간에 새 요소를 삽입하지 않은 경우에만 유효합니다. LinkedList의는 이러한 제한이없는 경우에 당신이에 잡아 노드 (사용할 node.Value원본 요소를 원할 때). 따라서 원시 값이 아닌 노드와 작동하도록 알고리즘을 다시 작성하십시오.
ToolmakerSteve

0

위의 내용에 동의합니다. 또한 대부분의 경우 List가 더 확실한 선택 인 것 같습니다.

그러나 LinkedList가 효율을 높이기 위해 List보다 훨씬 나은 인스턴스가 많이 있다고 덧붙이고 싶습니다.

  1. 요소를 탐색하고 많은 삽입 / 삭제를 수행하려고한다고 가정하십시오. LinkedList는 선형 O (n) 시간으로 수행하는 반면 List는 2 차 O (n ^ 2) 시간으로 수행합니다.
  2. 더 큰 객체에 반복해서 액세스하려고한다고 가정하면 LinkedList가 더욱 유용 해집니다.
  3. Deque () 및 queue ()는 LinkedList를 사용하여 더 잘 구현됩니다.
  4. 더 큰 객체를 다루면 LinkedList의 크기를 늘리는 것이 훨씬 쉽고 좋습니다.

누군가가 이러한 의견이 도움이 되길 바랍니다.


이 조언은 Java가 아닌 .NET에 대한 것입니다. Java의 링크 된 목록 구현에서는 "현재 노드"라는 개념이 없으므로 각 삽입마다 목록을 탐색해야합니다.
Jonathan Allen

이 대답은 부분적으로 만 정확합니다 .2) 요소가 큰 경우 요소 유형을 구조가 아닌 클래스로 지정하면 List가 단순히 참조를 보유합니다. 그런 다음 요소 크기와 관련이 없습니다. 3) 시작시 삽입 또는 제거를 수행하는 대신 목록을 "원형 버퍼"로 사용 하면 목록에서 큐 효율적으로 큐에 넣을 있습니다 . StephenCleary 's Deque . 4) 부분적으로 사실 : 많은 객체 인 경우 LL의 프로는 거대한 연속 메모리가 필요하지 않습니다. 단점은 노드 포인터를위한 추가 메모리입니다.
ToolmakerSteve

-2

여기에 많은 평균 답변이 있습니다 ...

일부 링크 된 목록 구현은 사전 할당 된 노드의 기본 블록을 사용합니다. 이들이 일정 시간 / 선형 시간보다이 작업을 수행하지 않으면 메모리 성능이 저하되고 캐시 성능이 훨씬 저하되므로 관련성이 떨어집니다.

다음과 같은 경우 링크 된 목록 사용

1) 나사산 안전을 원합니다. 더 나은 스레드 안전 알고리즘을 구축 할 수 있습니다. 잠금 비용은 동시 스타일 목록을 지배합니다.

2) 구조와 같은 큰 대기열이 있고 끝 부분을 제거하거나 항상 추가하려는 경우. > 100K 이상의 목록이 존재하지만 일반적이지 않습니다.


3
이 질문은 일반적으로 링크 된 목록이 아닌 두 개의 C # 구현에 관한 것입니다.
Jonathan Allen

모든 언어에서 동일
user1496062

-2

LinkedList 컬렉션의 성능과 관련 하여 비슷한 질문을 했고 Steven Cleary의 Deque C # 구현이 해결책이라는 것을 알았습니다 . 대기열 컬렉션과 달리 Deque를 사용하면 항목을 앞뒤로 이동할 수 있습니다. 링크 된 목록과 비슷하지만 성능이 향상되었습니다.


1
명세서 재 Deque이다 "연결리스트와 비슷한, 그러나 향상된 성능과 함께" . 그 문을 자격을주십시오 Deque보다 더 나은 성능이며 LinkedList, 특정 코드 . 링크를 따라 이틀 후 Ivan Stoev에서 이것이 LinkedList의 비효율이 아니라 코드의 비효율이라는 것을 알게되었습니다. (그리고 그것이 LinkedList의 비효율적 이었음에도 불구하고 Deque가 더 효율적이라는 일반적인 진술을 정당화하지는 않을 것입니다. 특정 경우에만)
ToolmakerSteve
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.