Чому ми використовуємо масиви замість інших структур даних?


196

Під час програмування я не бачив жодного випадку, коли масив краще зберігати інформацію, ніж інша форма. Я дійсно вважав, що додані "функції" мов програмування покращилися на цьому, і тим самим їх замінили. Зараз я бачу, що їх не замінюють, а дають нове життя, так би мовити.

Отже, в основному, який сенс використовувати масиви?

Це не стільки, чому ми використовуємо масиви з комп'ютерної точки зору, а навпаки, чому б ми використовували масиви з точки зору програмування (тонка різниця). Те, що робить комп’ютер із масивом, не було питанням.


2
Чому б не розглянути, що робить комп’ютер з масивом? У нас є система нумерації будинків, оскільки у нас ПРАВІ вулиці. Так це і для масивів.
lcn

Що ви маєте на увазі під " іншими структурами даних " чи " іншою формою "? І з якою метою?
тевемадар

Відповіді:


771

Час повернутися у часі до уроку. Хоча ми сьогодні не дуже багато думаємо про ці речі в наших фантазійних керованих мовах, вони побудовані на одній основі, тому давайте подивимось, як керується пам’яттю в C.

Перш ніж зануритися, коротке пояснення того, що означає термін « вказівник ». Вказівник - це просто змінна, яка "вказує" на місце в пам'яті. Він не містить фактичного значення в цій області пам'яті, він містить адресу пам'яті до нього. Подумайте про блок пам'яті як про поштову скриньку. Вказівник буде адресою цієї поштової скриньки.

У C масив - це просто вказівник зі зміщенням, зміщення вказує, наскільки в пам'яті слід шукати. Це забезпечує час доступу O (1) .

  MyArray   [5]
     ^       ^
  Pointer  Offset

Усі інші структури даних або надбудовуються на цьому, або не використовують суміжну пам'ять для зберігання, що призводить до поганого часу пошуку випадкового доступу (Хоча є й інші переваги, якщо не використовувати послідовну пам'ять).

Наприклад, скажімо, у нас є масив з 6 числами (6,4,2,3,1,5), в пам'яті він виглядатиме так:

=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================

У масиві ми знаємо, що кожен елемент знаходиться поруч один з одним у пам'яті. AC масив (називається MyArrayтут) - це просто вказівник на перший елемент:

=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================
   ^
MyArray

Якби ми хотіли подивитися MyArray[4], всередині нього можна було б отримати доступ до цього:

   0     1     2     3     4 
=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================
                           ^
MyArray + 4 ---------------/
(Pointer + Offset)

Оскільки ми можемо безпосередньо отримати доступ до будь-якого елемента в масиві, додавши зміщення до покажчика, ми можемо шукати будь-який елемент за однаковий час, незалежно від розміру масиву. Це означає, що отримання часу MyArray[1000]займає стільки ж часу, як і отримання MyArray[5].

Альтернативна структура даних - пов'язаний список. Це лінійний список покажчиків, кожен із яких вказує на наступний вузол

========    ========    ========    ========    ========
| Data |    | Data |    | Data |    | Data |    | Data |
|      | -> |      | -> |      | -> |      | -> |      | 
|  P1  |    |  P2  |    |  P3  |    |  P4  |    |  P5  |        
========    ========    ========    ========    ========

P(X) stands for Pointer to next node.

Зауважте, що я зробив кожен "вузол" у свій блок. Це тому, що вони не гарантують, що (і, швидше за все, не будуть) сусідніми в пам'яті.

Якщо я хочу отримати доступ до P3, я не можу отримати прямий доступ до нього, бо не знаю, де він знаходиться в пам'яті. Все, що я знаю, - де знаходиться корінь (P1), тому замість цього я повинен почати з P1 і слідувати за кожним вказівником на потрібний вузол.

Це час пошуку О (N) (вартість пошуку збільшується із додаванням кожного елемента). Дістатися до P1000 набагато дорожче порівняно з доїздом до P4.

Структури даних вищого рівня, такі як хештелі, стеки та черги, усі можуть використовувати масив (або декілька масивів) внутрішньо, тоді як пов'язані списки та двійкові дерева зазвичай використовують вузли та покажчики.

Вам може бути цікаво, чому хтось використовуватиме структуру даних, яка потребує лінійного обходу, щоб шукати значення, а не просто використовувати масив, але вони мають своє використання.

Знову візьміть наш масив. Цього разу я хочу знайти елемент масиву, який містить значення '5'.

=====================================
|  6  |  4  |  2  |  3  |  1  |  5  |
=====================================
   ^     ^     ^     ^     ^   FOUND!

У цій ситуації я не знаю, який зсув додати до вказівника, щоб знайти його, тому я повинен почати з 0 і працювати вгору, поки не знайду його. Це означає, що я повинен виконати 6 перевірок.

Через це пошук значення в масиві вважається O (N). Вартість пошуку збільшується в міру збільшення масиву.

Згадайте вище, де я говорив, що іноді використання непослідовної структури даних може мати переваги? Пошук даних є однією з цих переваг, і один з найкращих прикладів - Бінарне дерево.

Бінарне дерево - це структура даних, схожа на зв'язаний список, однак замість посилання на один вузол, кожен вузол може посилатися на два дочірні вузли.

         ==========
         |  Root  |         
         ==========
        /          \ 
  =========       =========
  | Child |       | Child |
  =========       =========
                  /       \
            =========    =========
            | Child |    | Child |
            =========    =========

 Assume that each connector is really a Pointer

Коли дані вставляються у двійкове дерево, воно використовує кілька правил, щоб вирішити, де розмістити новий вузол. Основна концепція полягає в тому, що якщо нове значення більше, ніж у батьків, воно вставляє його зліва, якщо воно нижче, воно вставляє його праворуч.

Це означає, що значення у двійковому дереві можуть виглядати так:

         ==========
         |   100  |         
         ==========
        /          \ 
  =========       =========
  |  200  |       |   50  |
  =========       =========
                  /       \
            =========    =========
            |   75  |    |   25  |
            =========    =========

Під час пошуку бінарного дерева на значення 75 нам потрібно відвідати лише 3 вузли (O (log N)) через цю структуру:

  • На 75 менше 100? Подивіться на Правий вузол
  • 75 більше 50? Подивіться на Лівий вузол
  • Є 75!

Незважаючи на те, що на нашому дереві є 5 вузлів, нам не потрібно було дивитись на два інших, оскільки ми знали, що вони (та їхні діти) не можуть містити значення, яке ми шукали. Це дає нам час пошуку, що в гіршому випадку означає, що ми повинні відвідувати кожен вузол, але в кращому випадку нам потрібно лише відвідати невелику частину вузлів.

Ось де масиви перебиваються, вони забезпечують лінійний час пошуку O (N), незважаючи на час доступу O (1).

Це неймовірно високий огляд структур даних в пам'яті, пропускаючи багато деталей, але, сподіваємось, він ілюструє силу та слабкість масиву порівняно з іншими структурами даних.


1
@Jonathan: Ви оновили діаграму, щоб вона вказувала на 5-й елемент, але ви також змінили MyArray [4] на MyArray [5], тому це все ще неправильно, змініть індекс назад на 4 і збережіть діаграму такою, якою є, і ви повинні бути хорошими .
Роберт Гембл

54
Це те, що клопоче мене про "Вікі спільноти", цей пост вартий "належного" реп.
Химерний

8
Гарна відповідь. Але дерево, яке ви описуєте, є двійковим деревом пошуку - бінарне дерево - це просто дерево, де кожен вузол має щонайбільше двох дітей. Ви можете мати двійкове дерево з елементами в будь-якому порядку. Двійкове дерево пошуку організоване так, як ви описуєте.
gnud

1
Добре пояснення, але я не можу допомогти nitpick ... якщо вам дозволено переупорядкувати елементи у двійковому дереві пошуку, чому ви не можете переупорядкувати елементи масиву, щоб і двійковий пошук працював у ньому? Ви можете розглянути більш детальну інформацію про O (n) вставлення / видалення для дерева, але O (n) для масиву.
ринки

2
Чи не є представлення бінарного дерева O (log n), оскільки час доступу збільшується логарифмічно щодо розміру набору даних?
Еван Плейс

73

Для O (1) випадковий доступ, який неможливо побити.


6
На якій точці? Що таке O (1)? Що таке випадковий доступ? Чому її не можна побити? Ще один момент?
Жасон

3
O (1) означає постійний час, наприклад, якщо ви хочете отримати n-esim елемент масиву, ви просто отримаєте доступ до нього безпосередньо через його індексатор (масив [n-1]), наприклад, з пов'язаним списком, у вас є щоб знайти голову, а потім перейти до наступного вузла послідовно n-1 разів, що становить O (n), лінійний час.
CMS

8
Нотація Big-O описує, як змінюється швидкість алгоритму залежно від розміру його введення. Алгоритм O (n) буде запускати вдвічі більше, ніж запустити вдвічі більше елементів, і в 8 разів більше, ніж запустити з 8 разів більше елементів. Іншими словами, швидкість роботи алгоритму O (n) змінюється залежно від [продовження ...]
Гарет

8
розмір його вводу. O (1) означає, що розмір вводу ('n') не враховує швидкість алгоритму, це постійна швидкість незалежно від розміру вводу
Гарет

9
Я бачу ваш O (1) і піднімаю вас O (0).
Кріс Конвей

23

Не всі програми роблять те саме або працюють на одному і тому ж апаратному забезпеченні.

Зазвичай це відповідь, чому існують різні мовні особливості. Масиви - це основна концепція інформатики. Заміна масивів списками / матрицями / векторами / будь-якою розширеною структурою даних сильно вплине на продуктивність і буде прямо нездійсненною у ряді систем. Існує будь-яка кількість випадків, коли використання одного з цих "розширених" об'єктів збору даних слід використовувати через цю програму.

У бізнес-програмуванні (яким займається більшість з нас) ми можемо орієнтуватися на обладнання, яке є досить потужним. Використання списку в C # або Vector в Java - це правильний вибір, який можна зробити в цих ситуаціях, оскільки ці структури дозволяють розробнику швидше виконувати цілі, що, в свою чергу, дозволяє більш ефективно використовувати цей тип програмного забезпечення.

При написанні вбудованого програмного забезпечення або операційної системи масив часто може бути кращим вибором. Хоча масив пропонує меншу функціональність, він займає менше оперативної пам’яті, а компілятор може оптимізувати код ефективніше для пошуку в масиви.

Я впевнений, що я залишаю безліч переваг для цих випадків, але сподіваюся, ви зрозумієте, що ви зрозуміли.


4
За іронією долі, в Java ви повинні використовувати ArrayList (або LinkedList) замість вектора. Це пов'язано з синхронізованим вектором, який зазвичай є зайвим накладними.
ashirley

0

Спосіб розглянути переваги масивів - це побачити, де потрібна можливість доступу до масивів O (1) і, отже, з великої літери:

  1. У таблицях огляду програми (статичний масив для доступу до певних категоричних відповідей)

  2. Пам'ять (вже обчислені результати складної функції, так що ви більше не обчислюєте значення функції, скажімо log x)

  3. Високошвидкісні програми для комп’ютерного зору, які потребують обробки зображень ( https://en.wikipedia.org/wiki/Lookup_table#Lookup_tables_in_image_processing )

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.