언제 List 대 LinkedList 를 사용하는 것이 더 낫 습니까?
언제 List 대 LinkedList 를 사용하는 것이 더 낫 습니까?
답변:
이 답변에 대한 의견을 읽으십시오. 사람들은 내가 적절한 테스트를하지 않았다고 주장합니다. 나는 이것이 받아 들여서는 안된다는 데 동의합니다. 배우면서 몇 가지 테스트를 수행하고 공유하는 느낌이 들었습니다.
흥미로운 결과를 찾았습니다.
// 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;
}
}
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;
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를 사용하지 않는다고 말한다.
다음은 많은 인서트를 수행하는 또 다른 비교입니다 (목록 중간에 항목을 삽입 할 계획입니다)
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;
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;
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;
그래서 단지 당신이 여러 항목을 삽입 계획하고있는 경우 도 어딘가에는 다음 링크 된 목록을 사용하여 항목을 삽입 할 계획 곳의 레퍼런스를 가지고있다. 많은 항목을 삽입해야하기 때문에 삽입하려는 위치를 검색하면 시간이 걸리기 때문에 더 빠르지 않습니다.
list.AddLast(a);
마지막 두 LinkedList 예제에서 왜 루프 가 발생합니까? list.AddLast(new Temp(1,1,1,1));
마지막 LinkedList 옆에있는 것처럼 루프 전에 한 번 수행 하지만 루프 자체에 Temp 객체를 두 배 더 추가하는 것처럼 보입니다. (그리고 내가 LinkedList에서 두 배나 많은 테스트 앱으로 나 자신을 다시 확인할 때 .)
I say never use a linkedList.
나중의 게시물에서 알 수 있듯이 일반적인 조언 에는 결함이 있습니다. 편집하고 싶을 수도 있습니다. 2) 타이밍은 무엇입니까? 한 번에 인스턴스화, 추가 및 열거? 대부분의 인스턴스화와 열거는 ppl이 걱정하는 것이 아니며 한 단계입니다. 특히 인서트와 추가 타이밍을 맞추는 것이 더 좋은 아이디어가 될 것입니다. 3) 가장 중요한 것은 링크리스트에 필요한 것 이상을 추가하는 것입니다. 이것은 잘못된 비교입니다. linkedlist에 대한 잘못된 아이디어가 퍼졌습니다.
대부분의 경우 List<T>
더 유용합니다. LinkedList<T>
목록 중간에 항목을 추가 / 제거 할 때 비용이 적게 드는 반면 목록 끝에List<T>
값을 추가 / 제거 할 수 있습니다 .
LinkedList<T>
순차 또는 역순으로 순차 데이터에 액세스하는 경우에만 가장 효율적입니다. 랜덤 액세스는 매번 체인을 걸어야하기 때문에 상대적으로 비용이 많이 듭니다 (따라서 인덱서가없는 이유). 그러나 a List<T>
는 본질적으로 배열 (래퍼 포함)이므로 임의 액세스가 좋습니다.
List<T>
또한 지원 방법을 많이 제공합니다 - Find
, ToArray
등; 그러나 LinkedList<T>
확장 방법을 통해 .NET 3.5 / C # 3.0 에서도 사용할 수 있으므로 그다지 중요하지 않습니다.
List<T>
및 T[]
(모두 한 슬래브) 너무 땅딸막 한 것에 대해 실패합니다 LinkedList<T>
(요소마다 슬래브) 너무 세부적인 것에 대해 통곡 할 것이다.
연결된 목록을 목록으로 생각하는 것은 약간 오해의 소지가 있습니다. 체인과 비슷합니다. 실제로 .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>
삽입 / 제거 후 목록의 추가 항목을 뒤섞어 야하므로 선형 시간을 제공합니다.
연결된 목록은 목록 구성원을 매우 빠르게 삽입하거나 삭제합니다. 링크 된 목록의 각 멤버에는 목록의 다음 멤버에 대한 포인터가 포함되어 i 위치에 멤버를 삽입합니다.
연결된 목록의 단점은 임의 액세스가 불가능하다는 것입니다. 멤버에 액세스하려면 원하는 멤버를 찾을 때까지 목록을 탐색해야합니다.
내 이전 답변이 충분히 정확하지 않았습니다. 정말 끔찍했습니다 : D 그러나 지금은 훨씬 더 유용하고 정확한 답변을 게시 할 수 있습니다.
추가 테스트를했습니다. https://github.com/ukushu/DataStructuresTestsAndOther.git 링크를 통해 소스를 찾아 환경에서 다시 확인할 수 있습니다.
짧은 결과 :
배열은 다음을 사용해야합니다.
목록을 사용해야합니다.
LinkedList는 다음을 사용해야합니다.
자세한 내용은:
LinkedList<T>
internally는 .NET의 List가 아닙니다. 심지어 구현하지도 않습니다 IList<T>
. 이것이 인덱스와 관련된 인덱스와 메소드가없는 이유입니다.
LinkedList<T>
노드 포인터 기반 컬렉션입니다. .NET에서는 이중 링크 구현입니다. 이는 이전 / 다음 요소가 현재 요소에 연결되어 있음을 의미합니다. 또한 데이터가 조각화되어 다른 RAM의 다른 위치에 다른 목록 개체를 배치 할 수 있습니다. 또한 LinkedList<T>
for List<T>
또는 Array 보다 더 많은 메모리가 사용됩니다 .
List<T>
.Net에서 Java의 대안입니다 ArrayList<T>
. 이것은 이것이 배열 래퍼임을 의미합니다. 따라서 하나의 연속 된 데이터 블록으로 메모리에 할당됩니다. 할당 된 데이터 크기가 85000 바이트를 초과하면 Large Object Heap으로 이동합니다. 크기에 따라 힙 조각화 (가벼운 형태의 메모리 누수)가 발생할 수 있습니다. 그러나 크기가 85000 바이트 미만인 경우 메모리에서 매우 작고 빠른 액세스 표현을 제공합니다.
랜덤 액세스 성능과 메모리 소비에는 단일 연속 블록이 선호되지만 정기적으로 크기를 변경해야하는 컬렉션의 경우 일반적으로 Array와 같은 구조를 새 위치로 복사해야하지만 연결된 목록은 새로 삽입 된 메모리 만 관리하면됩니다. / 삭제 된 노드.
List와 LinkedList의 차이점은 기본 구현에 있습니다. List는 배열 기반 컬렉션 (ArrayList)입니다. LinkedList는 노드 포인터 기반 콜렉션 (LinkedListNode)입니다. API 레벨 사용법에서 ICollection, IEnumerable 등과 같은 동일한 인터페이스 세트를 구현하므로 둘 다 거의 동일합니다.
중요한 차이점은 성능이 중요합니다. 예를 들어, "INSERT"작업이 많은 목록을 구현하는 경우 LinkedList가 List보다 성능이 우수합니다. LinkedList는 O (1) 시간에이를 수행 할 수 있지만 List는 기본 배열의 크기를 확장해야 할 수도 있습니다. 자세한 정보 / 세부 사항은 LinkedList와 배열 데이터 구조의 알고리즘 차이를 읽으십시오. http://en.wikipedia.org/wiki/Linked_list 및 배열
이 도움을 바랍니다.
Add
은 항상 기존 배열의 끝에 있는 경우에 대해 생각하고 있습니다. List
O (1)이 아니더라도 "충분히"좋습니다. 끝 Add
이 아닌 많은 것들이 필요한 경우 심각한 문제가 발생합니다 . Marc는 크기를 조정해야 할 때뿐만 아니라 삽입 할 때마다 기존 데이터 를 이동 해야하는 비용이보다 실질적인 비용이라고 지적합니다 . List
배열에 대한 링크 된 목록의 주요 장점은 링크가 항목을 효율적으로 재 배열 할 수있는 기능을 제공한다는 것입니다. 세지윅, p. 91
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();
}
RemoveAll
에서 항목을 제거 List
항목이 여러 이동하지 않고, 사용하거나 Where
두 번째 목록을 만들 수 LINQ에서. LinkedList
그러나 here를 사용하면 다른 유형의 컬렉션보다 훨씬 더 많은 메모리를 소비 하게 되며 메모리 지역성이 손실되면 반복하는 데 눈에 띄게 느려서 a보다 약간 나빠집니다 List
.
RemoveAll
Java에 동등한 것이 있는지 확실하지 않습니다 .
RemoveAll
사용할 수없는 경우 List
Tom의 루프처럼 보이는 "압축"알고리즘을 수행 할 수 있지만 두 개의 인덱스와 목록의 내부 배열에서 한 번에 하나씩 유지되도록 항목을 이동해야합니다. 효율성은 O (n)이며에 대한 Tom의 알고리즘과 동일합니다 LinkedList
. 두 버전 모두에서 문자열에 대한 HashSet 키를 계산하는 시간이 지배적입니다. 이것은 언제 사용해야하는 좋은 예가 아닙니다 LinkedList
.
기본적 List<>
으로 .NET의 배열 은 배열 의 래퍼 입니다. A LinkedList<>
는 연결된 목록 입니다. 따라서 문제는 배열과 연결 목록의 차이점과 연결 목록 대신 배열을 사용해야하는 시점에 관한 것입니다. 아마도 당신의 결정에있어서 가장 중요한 두 가지 요소는 다음과 같습니다.
이것은 몇 가지 잘못된 측정을 수정하는 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) 랜덤 액세스와 같이 더 나은 성능을 발휘하는 다른 영역이 있습니다.
LinkedList<>
때 사용
Token Stream
.다른 모든 것에는을 사용하는 것이 좋습니다 List<>
.
LinkedListNode<T>
코드 에서 객체를 추적하는 경우 삽입 / 삭제를 위해 목록을 스캔 할 필요가 없습니다 . 그렇게 할 수 있다면 List<T>
, 특히 삽입 / 제거가 빈번한 매우 긴 목록의 경우를 사용하는 것보다 훨씬 낫습니다 .
node.Value
원본 요소를 원할 때). 따라서 원시 값이 아닌 노드와 작동하도록 알고리즘을 다시 작성하십시오.
위의 내용에 동의합니다. 또한 대부분의 경우 List가 더 확실한 선택 인 것 같습니다.
그러나 LinkedList가 효율을 높이기 위해 List보다 훨씬 나은 인스턴스가 많이 있다고 덧붙이고 싶습니다.
누군가가 이러한 의견이 도움이 되길 바랍니다.
여기에 많은 평균 답변이 있습니다 ...
일부 링크 된 목록 구현은 사전 할당 된 노드의 기본 블록을 사용합니다. 이들이 일정 시간 / 선형 시간보다이 작업을 수행하지 않으면 메모리 성능이 저하되고 캐시 성능이 훨씬 저하되므로 관련성이 떨어집니다.
다음과 같은 경우 링크 된 목록 사용
1) 나사산 안전을 원합니다. 더 나은 스레드 안전 알고리즘을 구축 할 수 있습니다. 잠금 비용은 동시 스타일 목록을 지배합니다.
2) 구조와 같은 큰 대기열이 있고 끝 부분을 제거하거나 항상 추가하려는 경우. > 100K 이상의 목록이 존재하지만 일반적이지 않습니다.
LinkedList 컬렉션의 성능과 관련 하여 비슷한 질문을 했고 Steven Cleary의 Deque C # 구현이 해결책이라는 것을 알았습니다 . 대기열 컬렉션과 달리 Deque를 사용하면 항목을 앞뒤로 이동할 수 있습니다. 링크 된 목록과 비슷하지만 성능이 향상되었습니다.
Deque
이다 "연결리스트와 비슷한, 그러나 향상된 성능과 함께" . 그 문을 자격을주십시오 Deque
보다 더 나은 성능이며 LinkedList
, 특정 코드 . 링크를 따라 이틀 후 Ivan Stoev에서 이것이 LinkedList의 비효율이 아니라 코드의 비효율이라는 것을 알게되었습니다. (그리고 그것이 LinkedList의 비효율적 이었음에도 불구하고 Deque가 더 효율적이라는 일반적인 진술을 정당화하지는 않을 것입니다. 특정 경우에만)