STL Что такое произвольный доступ и последовательный доступ?

Поэтому мне любопытно узнать, что такое произвольный доступ?

Я искал немного, и не мог найти много. Теперь я понимаю, что «блоки» в контейнере размещаются случайным образом (как показано здесь). Тогда произвольный доступ означает, что я могу получить доступ к каждому блоку контейнера, независимо от того, в какой позиции (так что я могу прочитать, что он говорит в позиции 5, не проходя перед этим все блоки), в то время как при последовательном доступе я должен пройти через 1-й, 2-й, 3-й и 4-й, чтобы добраться до 5-го блока.

Я прав? Или если нет, то может кто-нибудь объяснить мне, что такое произвольный доступ и последовательный доступ?


person Sumsar1812    schedule 10.06.2014    source источник
comment
Да, вы правы в том, как они доступны. Но я не уверен, что вы подразумеваете под блоки в контейнере размещаются случайным образом. Что бы это ни значило, звучит неправильно.   -  person Benjamin Lindley    schedule 10.06.2014
comment
@BenjaminLindley спасибо!   -  person Sumsar1812    schedule 10.06.2014
comment
Меня не волнует, как это определяется в учебнике. Если ваше определение выше верно, то я думаю, что Iterator на самом деле является последовательным доступом, а не случайным доступом. Однако это не означает, что массив не является произвольным доступом.   -  person windmaomao    schedule 03.01.2021


Ответы (3)


Последовательный доступ означает, что стоимость доступа к 5-му элементу в 5 раз превышает стоимость доступа к первому элементу, или, по крайней мере, увеличивается стоимость, связанная с позицией элементов в наборе. Это связано с тем, что для доступа к 5-му элементу набора необходимо сначала выполнить операцию по поиску 1-го, 2-го, 3-го и 4-го элементов, поэтому для доступа к 5-му элементу требуется 5 операций.

Произвольный доступ означает, что доступ к любому элементу набора имеет ту же стоимость, что и доступ к любому другому элементу набора. Поиск 5-го элемента множества по-прежнему является единственной операцией.

Таким образом, доступ к случайному элементу в структуре данных с произвольным доступом будет иметь стоимость O (1), тогда как доступ к случайному элементу в последовательной структуре данных будет иметь стоимость O (n/2) -> O (n) . n/2 исходит из того, что если вы хотите получить доступ к случайному элементу в наборе 100 раз, средняя позиция этого элемента будет примерно на полпути через набор. Таким образом, для набора из n элементов получается n/2 (что в большой нотации O можно просто приблизить к n).

Что-то, что вы можете найти интересным:

Хеш-карты — это пример структуры данных, реализующей произвольный доступ. Интересно отметить, что при коллизиях хэшей в хеш-карте два столкнувшихся элемента сохраняются в последовательном связанном списке в этом сегменте на хэш-карте. Это означает, что если у вас есть 100% коллизий для хеш-карты, вы фактически получаете последовательное хранилище.

Вот изображение хэш-карты, иллюстрирующее то, что я описываю:

Hashmap

Это означает, что наихудший сценарий для хэш-карты на самом деле равен O(n) для доступа к элементу, так же, как и средний случай для последовательного хранения, или, говоря более правильно, поиск элемента в хэш-карте равен Ω(n), O(1). ) и Θ(1). Где Ω — наихудший случай, Θ — наилучший случай, а O — средний случай.

So:

Последовательный доступ: поиск случайного элемента в наборе из n элементов равен Ω(n), O(n/2) и Θ(1), что для очень больших чисел становится Ω(n), O (n) и Θ(1).

Произвольный доступ: поиск случайного элемента в наборе из n элементов равен Ω(n/2), O(1) и Θ(1), что для очень больших чисел становится Ω(n), O (1) и Θ(1)

Таким образом, произвольный доступ обеспечивает более высокую производительность при доступе к элементам, однако последовательные структуры данных хранения обеспечивают преимущества в других областях.

Второе редактирование для @sumsar1812:

Я хочу начать с того, как я понимаю преимущества/варианты использования последовательного хранилища, но я не так уверен в своем понимании преимуществ последовательных контейнеров, как в своем ответе выше. Поэтому, пожалуйста, поправьте меня, где я ошибаюсь.

Последовательное хранение полезно, потому что данные будут храниться в памяти последовательно.

На самом деле вы можете получить доступ к следующему элементу последовательно сохраненного набора данных, сместив указатель на предыдущий элемент этого набора на количество байтов, необходимое для хранения одного элемента этого типа.

Итак, поскольку для хранения подписанного int требуется 8 байтов, если у вас есть фиксированный массив целых чисел с указателем, указывающим на первое целое число:

int someInts[5];
someInts[1] = 5;

someInts — это указатель, указывающий на первый элемент этого массива. Добавление 1 к этому указателю просто смещает место, на которое он указывает в памяти, на 8 байт.

(someInts+1)* //returns 5

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

Для хранилища с произвольным доступом нет гарантии, что каждый элемент хранится даже рядом с другими элементами. Это означает, что каждый поиск будет дороже, чем простое добавление постоянной суммы.

Контейнеры хранения с произвольным доступом по-прежнему могут имитировать то, что выглядит как упорядоченный список элементов, с помощью итератора. Однако, пока вы разрешаете поиск элементов с произвольным доступом, нет никакой гарантии, что эти элементы будут храниться в памяти последовательно. Это означает, что хотя контейнер может демонстрировать поведение как контейнера с произвольным доступом, так и последовательного контейнера, он не будет демонстрировать преимущества последовательного контейнера.

Таким образом, если порядок элементов в вашем контейнере должен быть значимым или вы планируете выполнять итерации и работать с каждым элементом в наборе данных, вам может быть полезен последовательный контейнер.

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

person echochamber    schedule 10.06.2014
comment
не правильнее ли было бы сказать O(1) -> O(n) в качестве возможных решений? для последовательных - person Sumsar1812; 10.06.2014
comment
Когда я говорю O(n/2) -> O(n), я подразумеваю, что по мере того, как n стремится к бесконечности (или какому-то абсурдно большому числу), тот факт, что вы ныряете на 2, становится более или менее неуместным, и O (n/2) приближается к O(n). Прошу прощения, так как это было несколько двусмысленно. - person echochamber; 10.06.2014
comment
Не могли бы вы объяснить некоторые области, в которых последовательное хранение дает преимущества? - person Sumsar1812; 11.06.2014
comment
@Sumsar1812 Sumsar1812 Я обновил свой ответ тем, что понял. Тем не менее, я не так уверен в этом ответе, как в части моего предыдущего ответа, поэтому я могу ошибаться в некоторых моментах. - person echochamber; 11.06.2014
comment
Вы переопределяете, что означают большие O, большие Theta и большие Omega, что делает этот ответ запутанным. - person Calicoder; 30.10.2018
comment
Ваши определения BigO, BigTheta и BigOmega неверны, они всего лишь границы некоторой математической функции, поэтому даже в худшем случае есть bigO, bigTheta и BigOmega - person Eduardo Sebastian; 19.05.2021

В этом есть два основных аспекта, и неясно, какой из них более актуален для вашего вопроса. Одним из таких аспектов является доступ к содержимому контейнера STL через итераторы, где эти итераторы обеспечивают либо произвольный доступ, либо прямой (последовательный) доступ. Другим аспектом является доступ к контейнеру или даже к самой памяти в случайном или последовательном порядке.

Итераторы — произвольный доступ против последовательного доступа

Чтобы начать с итераторов, возьмем два примера: std::vector<T> и std::list<T>. Вектор хранит массив значений, тогда как список хранит связанный список значений. Первый хранится в памяти последовательно, и это обеспечивает произвольный произвольный доступ: вычисление местоположения любого элемента происходит так же быстро, как и вычисление местоположения следующего элемента. Таким образом, последовательное хранилище обеспечивает эффективный произвольный доступ, а итератор — это итератор произвольного доступа< /эм>.

Напротив, список выполняет отдельное распределение для каждого узла, и каждый узел знает только, где находятся его соседи. Таким образом, вычисление местоположения случайного несоседнего узла не может быть выполнено напрямую. Любая попытка сделать это должна пройти через все промежуточные узлы, и поэтому алгоритмы, пытающиеся пропустить узлы, могут плохо работать. Непоследовательное хранение дает рандомизированные местоположения и, следовательно, только эффективный последовательный доступ. Таким образом, итератор, который предоставляет список, является двунаправленным итератором, один из нескольких различных последовательных итераторов.

Память — произвольный доступ против последовательного доступа

Однако в вашем вопросе есть еще одна морщинка. Части итератора касаются только обхода контейнера. Однако под этим ЦП будет обращаться к самой памяти по определенному шаблону. В то время как на высоком уровне ЦП способен адресовать любой случайный адрес без накладных расходов на вычисление, где он находится (это похоже на большой вектор), на практике чтение памяти включает в себя кэширование и множество тонкостей, из-за которых доступ к различным частям памяти занимает разные объемы. времени.

Например, когда вы начинаете работать с довольно большим набором данных, даже если вы работаете с вектором, более эффективно обращаться ко всем элементам в последовательном порядке, чем обращаться ко всем элементам в произвольном порядке. Напротив, список не делает это возможным. Поскольку узлы списка даже не обязательно расположены в последовательных ячейках памяти, даже последовательный доступ к элементам списка может не считывать память последовательно и из-за этого может быть более дорогим.

person Michael Urman    schedule 10.06.2014
comment
Я заявляю, чтобы проверить свое собственное понимание, поэтому, пожалуйста, поправьте меня, если я ошибаюсь. Так что это причина того, что доступ к элементам в массиве фиксированного размера более эффективен, чем вектор, потому что, поскольку вся память для массива фиксированного размера выделяется сразу, вы можете гарантировать, что каждый элемент массива фактически сохраняется последовательно в памяти ? Где, как с чем-то вроде вектора, перебирающего его, только моделируется поведение последовательно сохраненных элементов (поскольку некоторые из них могли быть выделены в разное время?) - person echochamber; 10.06.2014
comment
@echochamber Нет, выделение для вектора эквивалентно массиву, поскольку вектор перераспределяет и копирует/перемещает все элементы, если ему необходимо изменить размер своего хранилища, поэтому они должны быть эквивалентны (если вы не используете vec.at(n) или у вас есть компилятор, который плохо справляется с оптимизации). Важное различие между, скажем, вектором и списком. - person Michael Urman; 10.06.2014
comment
А, это имеет смысл. Благодарю за разъяснение. - person echochamber; 10.06.2014
comment
@MichaelUrman: Очень редко можно услышать последовательный доступ с итераторами, обычно мы используем один из тегов: ввод-вывод, прямой двунаправленный или произвольный доступ. - person Mooing Duck; 10.06.2014
comment
@MooingDuck Это справедливо. Было бы лучше сформулировать это как последовательный обход? Например, [List предоставляет] двунаправленный итератор, один из типов итераторов, который обеспечивает последовательный обход. - person Michael Urman; 11.06.2014
comment
@MichaelUrman: я не уверен, что предлагаю. Я просто предположил, что он имел в виду память из-за слова «доступ», но вы правы, что он мог иметь в виду итераторы, и хорошо, что оба они обсуждаются. - person Mooing Duck; 11.06.2014

Сами термины не подразумевают каких-либо характеристик производительности, как говорит @echochamber. Термины относятся только к методу доступа.

«Произвольный доступ» относится к доступу к элементам в контейнере в произвольном порядке. std::vector — это пример контейнера C++, который отлично подходит для произвольного доступа. std::stack — это пример контейнера C++, который не допускает даже произвольного доступа.

«Последовательный доступ» относится к доступу к элементам по порядку. Это актуально только для заказанных контейнеров. Некоторые контейнеры лучше оптимизированы для последовательного доступа, чем для произвольного доступа, например std::list.

Вот код, показывающий разницу:

// random access. picking elements out, regardless of ordering or sequencing.
// The important factor is that we are selecting items by some kind of
// index.
auto a = some_container[25];
auto b = some_container[1];
auto c = some_container["hi"];

// sequential access. Here, there is no need for an index.
// This implies that the container has a concept of ordering, where an
// element has neighbors
for(container_type::iterator it = some_container.begin();
    it != some_container.end();
    ++ it)
{
    auto d = *it;
}
person tenfour    schedule 10.06.2014
comment
Нет, я сказал, что сами термины не подразумевают производительность. Просто сказать, что этот контейнер поддерживает произвольный доступ, ничего не говорит о его производительности. - person tenfour; 11.06.2014
comment
Чем больше я об этом думаю, тем больше вы правы, но трудно придумать что-нибудь O(n), что можно было бы назвать произвольным доступом, но я видел случайный доступ O(log(n)) , так что ваша точка зрения остается в силе. - person Mooing Duck; 11.06.2014
comment
Важно отметить, что sequential может относиться к хранению и доступу; например std::vector — это последовательный контейнер хранения с произвольным доступом, а std::list — последовательный контейнер хранения с последовательным доступом. - person wulfgarpro; 25.09.2016