mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-13 07:46:06 +00:00
build
This commit is contained in:
@@ -11,7 +11,7 @@ comments: true
|
||||
- Целочисленные типы `byte` , `short` , `int` , `long` .
|
||||
- Типы с плавающей точкой `float` , `double` , используемые для представления дробных чисел.
|
||||
- Символьный тип `char` , используемый для представления букв, знаков препинания и даже эмодзи в разных языках.
|
||||
- Логический тип `bool` , используемый для представления суждений "да" и "нет".
|
||||
- Логический тип `bool` , используемый для представления суждений «да» и «нет».
|
||||
|
||||
**Базовые типы данных хранятся в компьютере в двоичной форме**. Один двоичный разряд равен $1$ биту. В большинстве современных операционных систем $1$ байт (byte) состоит из $8$ битов (bit).
|
||||
|
||||
@@ -20,7 +20,7 @@ comments: true
|
||||
- Целочисленный тип `byte` занимает $1$ байт = $8$ бит и может представлять $2^{8}$ чисел.
|
||||
- Целочисленный тип `int` занимает $4$ байта = $32$ бита и может представлять $2^{32}$ чисел.
|
||||
|
||||
В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть; достаточно иметь общее представление и при необходимости обращаться к ней.
|
||||
В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть. Достаточно иметь общее представление и при необходимости обращаться к ней.
|
||||
|
||||
<p align="center"> Таблица 3-1 Объем памяти и диапазоны значений базовых типов данных </p>
|
||||
|
||||
@@ -39,18 +39,18 @@ comments: true
|
||||
|
||||
</div>
|
||||
|
||||
Обрати внимание: приведенная выше таблица относится именно к базовым типам данных Java. В каждом языке программирования свои определения типов, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться.
|
||||
Обрати внимание: в таблице 3-1 приведены данные именно для базовых типов данных Java. В каждом языке программирования свои определения типов, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться.
|
||||
|
||||
- В Python целочисленный тип `int` может иметь произвольный размер, ограниченный только доступной памятью; тип `float` является 64-битным числом двойной точности; типа `char` нет, а отдельный символ на деле является строкой `str` длины 1.
|
||||
- В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. таблица 3-1 соответствует модели данных LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), используемой в 64-битных Unix-системах, включая Linux и macOS.
|
||||
- Размер символа `char` в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов; подробнее это рассматривается в разделе "Кодирование символов".
|
||||
- В Python целочисленный тип `int` может иметь произвольный размер, ограниченный только доступной памятью. Тип `float` является 64-битным числом двойной точности. Типа `char` нет, а отдельный символ на деле является строкой `str` длины 1.
|
||||
- В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. В таблице 3-1 приведены данные для модели LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), используемой в 64-битных Unix-системах, включая Linux и macOS.
|
||||
- Размер символа `char` в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов. Подробнее это рассматривается в разделе «Кодирование символов».
|
||||
- Хотя для представления логического значения достаточно 1 бита ( $0$ или $1$ ), в памяти оно обычно хранится как 1 байт. Это связано с тем, что современные CPU обычно используют 1 байт как минимальную адресуемую единицу памяти.
|
||||
|
||||
Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - "структура", а не "данные".
|
||||
Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - «структура», а не «данные».
|
||||
|
||||
Если мы хотим представить "ряд чисел", то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые `int` , вещественные `float` или символы `char` , - к "структуре данных" отношения не имеет.
|
||||
Если мы хотим представить «ряд чисел», то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые `int` , вещественные `float` или символы `char` , - к «структуре данных» отношения не имеет.
|
||||
|
||||
Иными словами, **базовые типы данных задают "тип содержимого" данных, а структуры данных задают "способ организации" данных**. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая `int` , `float` , `char` , `bool` и т.д.
|
||||
Иными словами, **базовые типы данных задают «тип содержимого» данных, а структуры данных задают «способ организации» данных**. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая `int` , `float` , `char` , `bool` и т.д.
|
||||
|
||||
=== "Python"
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ comments: true
|
||||
|
||||
# 3.4 Кодирование символов *
|
||||
|
||||
В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных `char` не является исключением. Для представления символов необходимо задать "таблицу символов", которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.
|
||||
В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных `char` не является исключением. Для представления символов необходимо задать «таблицу символов», которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.
|
||||
|
||||
## 3.4.1 Таблица символов ASCII
|
||||
|
||||
<u>Код ASCII</u> - это самая ранняя таблица символов; ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).
|
||||
<u>Код ASCII</u> - это самая ранняя таблица символов. Ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -26,15 +26,15 @@ comments: true
|
||||
|
||||
## 3.4.3 Таблица символов Unicode
|
||||
|
||||
С бурным развитием компьютерной техники таблицы символов и стандарты кодирования начали стремительно множиться, и это породило множество проблем. С одной стороны, такие таблицы обычно определяли символы только для конкретных языков и не могли нормально работать в многоязычной среде. С другой стороны, для одного и того же языка существовало несколько стандартов кодирования; если две машины использовали разные стандарты, при обмене информацией возникали искажения текста.
|
||||
С бурным развитием компьютерной техники таблицы символов и стандарты кодирования начали стремительно множиться, и это породило множество проблем. С одной стороны, такие таблицы обычно определяли символы только для конкретных языков и не могли нормально работать в многоязычной среде. С другой стороны, для одного и того же языка существовало несколько стандартов кодирования. Если две машины использовали разные стандарты, при обмене информацией возникали искажения текста.
|
||||
|
||||
Исследователи той эпохи задумались: **если создать достаточно полную таблицу символов, которая включит все языки и знаки мира, разве это не решит проблемы многоязычной среды и искаженного текста**? Под влиянием этой идеи и появилась большая и всеобъемлющая таблица символов Unicode.
|
||||
|
||||
<u>Unicode</u> по-китайски называется "единый код" и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.
|
||||
<u>Unicode</u> по-китайски называется «единый код» и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.
|
||||
|
||||
Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную "кодовую точку" (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако **Unicode не определяет, как именно хранить эти кодовые точки в компьютере**. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?
|
||||
Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную «кодовую точку» (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако **Unicode не определяет, как именно хранить эти кодовые точки в компьютере**. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?
|
||||
|
||||
Для этой проблемы **прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины**. Как показано на рисунке 3-7, каждый символ в "Hello" занимает 1 байт, а каждый символ в "алгоритм" занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в "Hello алгоритм" в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.
|
||||
Для этой проблемы **прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины**. Как показано на рисунке 3-7, каждый символ в «Hello» занимает 1 байт, а каждый символ в «алгоритм» занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в «Hello алгоритм» в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -49,9 +49,9 @@ comments: true
|
||||
Правила кодирования UTF-8 не слишком сложны и делятся на два случая.
|
||||
|
||||
- Для символов длиной 1 байт старший бит устанавливается в $0$ , а оставшиеся 7 битов содержат кодовую точку Unicode. Стоит отметить, что символы ASCII занимают первые 128 кодовых точек в наборе Unicode. Иными словами, **кодировка UTF-8 обратно совместима с ASCII**. Это означает, что мы можем использовать UTF-8 для разбора очень старых ASCII-текстов.
|
||||
- Для символов длиной $n$ байт (где $n > 1$) старшие $n$ битов первого байта устанавливаются в $1$ , а $(n + 1)$-й бит устанавливается в $0$ ; начиная со второго байта, старшие 2 бита каждого байта устанавливаются в $10$ ; все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.
|
||||
- Для символов длиной $n$ байт (где $n > 1$) старшие $n$ битов первого байта устанавливаются в $1$ , а $(n + 1)$-й бит устанавливается в $0$. Начиная со второго байта, старшие 2 бита каждого байта устанавливаются в $10$. Все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.
|
||||
|
||||
На рисунке 3-8 показана UTF-8-кодировка для строки "Hello алгоритм". Можно заметить, что поскольку старшие $n$ битов установлены в $1$ , система может определить длину символа как $n$ , подсчитав число ведущих единиц.
|
||||
На рисунке 3-8 показана UTF-8-кодировка для строки «Hello алгоритм». Можно заметить, что поскольку старшие $n$ битов установлены в $1$ , система может определить длину символа как $n$ , подсчитав число ведущих единиц.
|
||||
|
||||
Но почему старшие 2 бита всех остальных байтов устанавливаются в $10$ ? На самом деле это $10$ играет роль контрольного маркера. Если система начнет разбирать текст с неверного байта, префикс $10$ поможет быстро обнаружить аномалию.
|
||||
|
||||
@@ -63,10 +63,10 @@ comments: true
|
||||
|
||||
Помимо UTF-8, распространены еще два следующих способа кодирования.
|
||||
|
||||
- **Кодировка UTF-16**: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами; небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.
|
||||
- **Кодировка UTF-16**: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами. Небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.
|
||||
- **Кодировка UTF-32**: каждый символ занимает 4 байта. Это означает, что UTF-32 требует больше места, чем UTF-8 и UTF-16, особенно в текстах с большой долей ASCII-символов.
|
||||
|
||||
С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт; а для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.
|
||||
С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт. А для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.
|
||||
|
||||
С точки зрения совместимости у UTF-8 наилучшая универсальность, и многие инструменты и библиотеки в первую очередь поддерживают именно UTF-8.
|
||||
|
||||
@@ -80,15 +80,15 @@ comments: true
|
||||
|
||||
Вообще говоря, проектирование схем кодирования символов в языках программирования - очень интересная тема, в которой учитывается множество факторов.
|
||||
|
||||
- Тип `String` в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой "суррогатной парой").
|
||||
- Тип `String` в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой «суррогатной парой»).
|
||||
- Строки в JavaScript и TypeScript используют UTF-16 по причинам, похожим на Java. Когда Netscape впервые выпустила JavaScript в 1995 году, Unicode еще находился на ранней стадии развития, и 16-битного кодирования тогда было достаточно для представления всех символов Unicode.
|
||||
- C# использует UTF-16 главным образом потому, что платформа .NET была разработана Microsoft, а многие технологии Microsoft (включая Windows) широко используют именно UTF-16.
|
||||
|
||||
Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать "суррогатные пары" для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.
|
||||
Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать «суррогатные пары» для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.
|
||||
|
||||
По этим причинам некоторые языки программирования предложили иные схемы кодирования.
|
||||
|
||||
- `str` в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт; если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта; если встречаются символы за пределами BMP, каждый символ занимает 4 байта.
|
||||
- `str` в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт. Если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта. Если встречаются символы за пределами BMP, каждый символ занимает 4 байта.
|
||||
- Тип `string` в Go внутри использует кодировку UTF-8. Язык Go также предоставляет тип `rune`, предназначенный для представления одной кодовой точки Unicode.
|
||||
- Типы `str` и `String` в Rust внутри используют UTF-8. В Rust также есть тип `char`, представляющий одну кодовую точку Unicode.
|
||||
|
||||
|
||||
@@ -8,17 +8,17 @@ comments: true
|
||||
|
||||
## 3.1.1 Логическая структура: линейная и нелинейная
|
||||
|
||||
**Логическая структура раскрывает логические отношения между элементами данных**. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения "предок" и "потомок". Графы состоят из вершин и ребер, отражая сложные сетевые отношения.
|
||||
**Логическая структура раскрывает логические отношения между элементами данных**. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения «предок» и «потомок». Графы состоят из вершин и ребер, отражая сложные сетевые отношения.
|
||||
|
||||
Как показано на рисунке 3-1, логические структуры делятся на две большие категории: линейные и нелинейные. Линейные структуры более интуитивны, поскольку в них данные расположены линейно и логически связаны. Нелинейные структуры, напротив, представляют собой нелинейное расположение элементов данных.
|
||||
|
||||
- **Линейные структуры данных**: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением "один к одному".
|
||||
- **Линейные структуры данных**: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением «один к одному».
|
||||
- **Нелинейные структуры данных**: деревья, кучи, графы, хеш-таблицы.
|
||||
|
||||
Нелинейные структуры данных можно дополнительно разделить на древовидные и сетевые.
|
||||
|
||||
- **Древовидные структуры**: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением "один ко многим".
|
||||
- **Сетевые структуры**: графы, в которых элементы связаны отношением "многие ко многим".
|
||||
- **Древовидные структуры**: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением «один ко многим».
|
||||
- **Сетевые структуры**: графы, в которых элементы связаны отношением «многие ко многим».
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -36,9 +36,9 @@ comments: true
|
||||
|
||||
!!! tip
|
||||
|
||||
Стоит отметить, что сравнение памяти с таблицей Excel - это упрощенная аналогия; реальный механизм работы памяти гораздо сложнее и включает такие понятия, как адресное пространство, управление памятью, кэш-механизмы, виртуальная и физическая память.
|
||||
Стоит отметить, что сравнение памяти с таблицей Excel - это упрощенная аналогия. Реальный механизм работы памяти гораздо сложнее и включает такие понятия, как адресное пространство, управление памятью, кэш-механизмы, виртуальная и физическая память.
|
||||
|
||||
Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. **Поэтому при проектировании структур данных и алгоритмов память занимает важное место**. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы; если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.
|
||||
Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. **Поэтому при проектировании структур данных и алгоритмов память занимает важное место**. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы. Если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.
|
||||
|
||||
Как показано на рисунке 3-3, **физическая структура отражает способ хранения данных в памяти компьютера**. Ее можно разделить на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Физическая структура на низком уровне определяет способы доступа к данным, их обновления, вставки и удаления. Эти два типа физических структур взаимно дополняют друг друга по временной и пространственной эффективности.
|
||||
|
||||
@@ -46,12 +46,12 @@ comments: true
|
||||
|
||||
<p align="center"> Рисунок 3-3 Хранение в непрерывном и разрозненном пространстве </p>
|
||||
|
||||
Стоит отметить, что **все структуры данных реализуются на основе массивов, связных списков или их комбинации**. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков; реализация хеш-таблицы также может одновременно включать массивы и связные списки.
|
||||
Стоит отметить, что **все структуры данных реализуются на основе массивов, связных списков или их комбинации**. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков. Реализация хеш-таблицы также может одновременно включать массивы и связные списки.
|
||||
|
||||
- **Можно реализовать на основе массивов**: стеки, очереди, хеш-таблицы, деревья, кучи, графы, матрицы, тензоры (массивы размерности $\geq 3$ ) и т.д.
|
||||
- **Можно реализовать на основе связных списков**: стеки, очереди, хеш-таблицы, деревья, кучи, графы и т.д.
|
||||
|
||||
После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют "динамической структурой данных". Длина массива после инициализации неизменна, поэтому его также называют "статической структурой данных". Стоит отметить, что массив может изменять длину за счет повторного выделения памяти, тем самым приобретая определенную "динамичность".
|
||||
После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют «динамической структурой данных». Длина массива после инициализации неизменна, поэтому его также называют «статической структурой данных». Стоит отметить, что массив может изменять длину за счет повторного выделения памяти, тем самым приобретая определенную «динамичность».
|
||||
|
||||
!!! tip
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ icon: material/shape-outline
|
||||
!!! abstract
|
||||
|
||||
Структуры данных подобны прочному и многообразному каркасу.
|
||||
|
||||
|
||||
Они задают схему упорядоченной организации данных, на основе которой оживают алгоритмы.
|
||||
|
||||
## Содержание главы
|
||||
|
||||
@@ -12,11 +12,11 @@ comments: true
|
||||
|
||||
В таблице из предыдущего раздела можно заметить, что все целочисленные типы могут представлять на одно отрицательное число больше, чем положительных. Например, диапазон `byte` равен $[-128, 127]$ . Это выглядит не слишком интуитивно, и внутренняя причина связана с прямым, обратным и дополнительным кодами.
|
||||
|
||||
Прежде всего нужно отметить, что **числа хранятся в компьютере в виде "дополнительного кода"**. Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.
|
||||
Прежде всего нужно отметить, что **числа хранятся в компьютере в виде «дополнительного кода»**. Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.
|
||||
|
||||
- **Прямой код**: старший бит двоичного представления числа рассматривается как знаковый, где $0$ означает положительное число, а $1$ - отрицательное; остальные биты представляют значение числа.
|
||||
- **Обратный код**: для положительного числа обратный код совпадает с прямым; для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.
|
||||
- **Дополнительный код**: для положительного числа дополнительный код совпадает с прямым; для отрицательного числа он получается добавлением $1$ к его обратному коду.
|
||||
- **Прямой код**: старший бит двоичного представления числа рассматривается как знаковый, где $0$ означает положительное число, а $1$ - отрицательное. Остальные биты представляют значение числа.
|
||||
- **Обратный код**: для положительного числа обратный код совпадает с прямым. Для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.
|
||||
- **Дополнительный код**: для положительного числа дополнительный код совпадает с прямым. Для отрицательного числа он получается добавлением $1$ к его обратному коду.
|
||||
|
||||
На рисунке 3-4 показаны способы преобразования между прямым, обратным и дополнительным кодами.
|
||||
|
||||
@@ -29,8 +29,8 @@ comments: true
|
||||
$$
|
||||
\begin{aligned}
|
||||
& 1 + (-2) \newline
|
||||
& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline
|
||||
& = 1000 \; 0011 \newline
|
||||
& \rightarrow 0000 \. 0001 + 1000 \. 0010 \newline
|
||||
& = 1000 \. 0011 \newline
|
||||
& \rightarrow -3
|
||||
\end{aligned}
|
||||
$$
|
||||
@@ -40,10 +40,10 @@ $$
|
||||
$$
|
||||
\begin{aligned}
|
||||
& 1 + (-2) \newline
|
||||
& \rightarrow 0000 \; 0001 \; \text{(прямой код)} + 1000 \; 0010 \; \text{(прямой код)} \newline
|
||||
& = 0000 \; 0001 \; \text{(обратный код)} + 1111 \; 1101 \; \text{(обратный код)} \newline
|
||||
& = 1111 \; 1110 \; \text{(обратный код)} \newline
|
||||
& = 1000 \; 0001 \; \text{(прямой код)} \newline
|
||||
& \rightarrow 0000 \. 0001 \. \text{(прямой код)} + 1000 \. 0010 \. \text{(прямой код)} \newline
|
||||
& = 0000 \. 0001 \. \text{(обратный код)} + 1111 \. 1101 \. \text{(обратный код)} \newline
|
||||
& = 1111 \. 1110 \. \text{(обратный код)} \newline
|
||||
& = 1000 \. 0001 \. \text{(прямой код)} \newline
|
||||
& \rightarrow -1
|
||||
\end{aligned}
|
||||
$$
|
||||
@@ -52,8 +52,8 @@ $$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
+0 & \rightarrow 0000 \; 0000 \newline
|
||||
-0 & \rightarrow 1000 \; 0000
|
||||
+0 & \rightarrow 0000 \. 0000 \newline
|
||||
-0 & \rightarrow 1000 \. 0000
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
@@ -61,36 +61,36 @@ $$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
-0 \rightarrow \; & 1000 \; 0000 \; \text{(прямой код)} \newline
|
||||
= \; & 1111 \; 1111 \; \text{(обратный код)} \newline
|
||||
= 1 \; & 0000 \; 0000 \; \text{(дополнительный код)} \newline
|
||||
-0 \rightarrow \. & 1000 \. 0000 \. \text{(прямой код)} \newline
|
||||
= \. & 1111 \. 1111 \. \text{(обратный код)} \newline
|
||||
= 1 \. & 0000 \. 0000 \. \text{(дополнительный код)} \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
При добавлении $1$ к обратному коду отрицательного нуля возникает перенос, но длина типа `byte` составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, **дополнительный код отрицательного нуля равен $0000 \; 0000$ и совпадает с дополнительным кодом положительного нуля**. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.
|
||||
При добавлении $1$ к обратному коду отрицательного нуля возникает перенос, но длина типа `byte` составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, **дополнительный код отрицательного нуля равен $0000 \. 0000$ и совпадает с дополнительным кодом положительного нуля**. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.
|
||||
|
||||
Остается последний вопрос: диапазон типа `byte` равен $[-128, 127]$ , откуда берется лишнее отрицательное число $-128$ ? Мы замечаем, что у всех целых чисел из интервала $[-127, +127]$ есть соответствующие прямой, обратный и дополнительный коды, а прямой и дополнительный коды можно преобразовывать друг в друга.
|
||||
|
||||
Однако **дополнительный код $1000 \; 0000$ является исключением: у него нет соответствующего прямого кода**. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен $0000 \; 0000$ . Это очевидное противоречие, потому что такой прямой код обозначает число $0$ , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код $1000 \; 0000$ представляет число $-128$ . На самом деле результат вычисления $(-1) + (-127)$ в дополнительном коде как раз и равен $-128$ .
|
||||
Однако **дополнительный код $1000 \. 0000$ является исключением: у него нет соответствующего прямого кода**. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен $0000 \. 0000$ . Это очевидное противоречие, потому что такой прямой код обозначает число $0$ , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код $1000 \. 0000$ представляет число $-128$ . На самом деле результат вычисления $(-1) + (-127)$ в дополнительном коде как раз и равен $-128$ .
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
& (-127) + (-1) \newline
|
||||
& \rightarrow 1111 \; 1111 \; \text{(прямой код)} + 1000 \; 0001 \; \text{(прямой код)} \newline
|
||||
& = 1000 \; 0000 \; \text{(обратный код)} + 1111 \; 1110 \; \text{(обратный код)} \newline
|
||||
& = 1000 \; 0001 \; \text{(дополнительный код)} + 1111 \; 1111 \; \text{(дополнительный код)} \newline
|
||||
& = 1000 \; 0000 \; \text{(дополнительный код)} \newline
|
||||
& \rightarrow 1111 \. 1111 \. \text{(прямой код)} + 1000 \. 0001 \. \text{(прямой код)} \newline
|
||||
& = 1000 \. 0000 \. \text{(обратный код)} + 1111 \. 1110 \. \text{(обратный код)} \newline
|
||||
& = 1000 \. 0001 \. \text{(дополнительный код)} + 1111 \. 1111 \. \text{(дополнительный код)} \newline
|
||||
& = 1000 \. 0000 \. \text{(дополнительный код)} \newline
|
||||
& \rightarrow -128
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Ты, вероятно, уже заметил, что все приведенные выше вычисления были операциями сложения. Это указывает на важный факт: **аппаратные схемы внутри компьютера в основном проектируются на основе операций сложения**. Причина в том, что сложение по сравнению с другими операциями (например умножением, делением и вычитанием) проще реализуется на аппаратном уровне, легче распараллеливается и выполняется быстрее.
|
||||
|
||||
Обрати внимание: это не означает, что компьютер умеет только складывать. **Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции**. Например, вычитание $a - b$ можно преобразовать в сложение $a + (-b)$ ; умножение и деление можно свести к многократному сложению или вычитанию.
|
||||
Обрати внимание: это не означает, что компьютер умеет только складывать. **Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции**. Например, вычитание $a - b$ можно преобразовать в сложение $a + (-b)$. Умножение и деление можно свести к многократному сложению или вычитанию.
|
||||
|
||||
Теперь можно подвести итог, почему компьютеры используют дополнительный код: с представлением в дополнительном коде компьютер может использовать одни и те же схемы и операции для сложения положительных и отрицательных чисел, без необходимости проектировать специальные аппаратные схемы для вычитания и без особой обработки неоднозначности положительного и отрицательного нуля. Это значительно упрощает аппаратную архитектуру и повышает эффективность вычислений.
|
||||
|
||||
Идея дополнительного кода очень изящна; из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.
|
||||
Идея дополнительного кода очень изящна. Из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.
|
||||
|
||||
## 3.3.2 Кодирование чисел с плавающей точкой
|
||||
|
||||
@@ -133,15 +133,15 @@ $$
|
||||
|
||||
<p align="center"> Рисунок 3-5 Пример вычисления float по стандарту IEEE 754 </p>
|
||||
|
||||
Посмотрим на рисунок 3-5: если взять пример $\mathrm{S} = 0$ , $\mathrm{E} = 124$ , $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ , то получим:
|
||||
Как видно на рисунке 3-5, если взять пример $\mathrm{S} = 0$ , $\mathrm{E} = 124$ , $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ , то получим:
|
||||
|
||||
$$
|
||||
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
|
||||
$$
|
||||
|
||||
Теперь мы можем ответить на исходный вопрос: **в представлении `float` присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у `int`**. Согласно приведенным выше вычислениям, максимально возможное положительное число для `float` равно $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$ ; если изменить бит знака, получим минимальное отрицательное число.
|
||||
Теперь мы можем ответить на исходный вопрос: **в представлении `float` присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у `int`**. Согласно приведенным выше вычислениям, максимально возможное положительное число для `float` равно $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$. Если изменить бит знака, получим минимальное отрицательное число.
|
||||
|
||||
**Хотя число с плавающей точкой `float` расширяет диапазон значений, побочным эффектом становится потеря точности**. Целочисленный тип `int` использует все 32 бита для представления числа, и числа распределены равномерно; а из-за существования битов экспоненты у `float` чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.
|
||||
**Хотя число с плавающей точкой `float` расширяет диапазон значений, побочным эффектом становится потеря точности**. Целочисленный тип `int` использует все 32 бита для представления числа, и числа распределены равномерно. А из-за существования битов экспоненты у `float` чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.
|
||||
|
||||
Как показано в таблице 3-2, значения экспоненты $\mathrm{E} = 0$ и $\mathrm{E} = 255$ имеют специальный смысл и **используются для представления нуля, бесконечности, $\mathrm{NaN}$ и т.д.**
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ comments: true
|
||||
|
||||
**Q**: Почему хеш-таблица одновременно включает линейные и нелинейные структуры данных?
|
||||
|
||||
В основе хеш-таблицы лежит массив, а для разрешения коллизий мы можем использовать "цепочки адресации" (об этом будет рассказано в последующем разделе "Хеш-коллизии"): каждый бакет массива указывает на связный список, а если длина списка превышает некоторый порог, он может быть преобразован в дерево (обычно в красно-черное дерево).
|
||||
В основе хеш-таблицы лежит массив, а для разрешения коллизий мы можем использовать «цепочки адресации» (об этом будет рассказано в последующем разделе «Хеш-коллизии»): каждый бакет массива указывает на связный список, а если длина списка превышает некоторый порог, он может быть преобразован в дерево (обычно в красно-черное дерево).
|
||||
|
||||
С точки зрения хранения данных в основе хеш-таблицы находится массив, где каждый слот бакета может содержать либо отдельное значение, либо связный список, либо дерево. Поэтому хеш-таблица действительно может одновременно включать линейные структуры данных (массивы, списки) и нелинейные структуры данных (деревья).
|
||||
|
||||
@@ -29,42 +29,42 @@ comments: true
|
||||
|
||||
Длина типа `char` определяется используемым в языке программирования способом кодирования. Например, Java, JavaScript, TypeScript и C# используют кодировку UTF-16 (для хранения кодовых точек Unicode), поэтому длина `char` у них равна 2 байтам.
|
||||
|
||||
**Q**: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются "статическими структурами данных"? Ведь стек тоже поддерживает операции push и pop, а они явно "динамические".
|
||||
**Q**: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются «статическими структурами данных»? Ведь стек тоже поддерживает операции push и pop, а они явно «динамические».
|
||||
|
||||
Стек действительно может поддерживать динамические операции над данными, но сама структура данных при этом остается "статической" (ее длина неизменна). Хотя структуры на основе массива могут динамически добавлять и удалять элементы, их емкость фиксирована. Если количество данных превышает заранее выделенный размер, приходится создавать новый, более крупный массив и копировать в него содержимое старого.
|
||||
Стек действительно может поддерживать динамические операции над данными, но сама структура данных при этом остается «статической» (ее длина неизменна). Хотя структуры на основе массива могут динамически добавлять и удалять элементы, их емкость фиксирована. Если количество данных превышает заранее выделенный размер, приходится создавать новый, более крупный массив и копировать в него содержимое старого.
|
||||
|
||||
**Q**: При построении стека (очереди) его размер не задается явно, почему же его относят к "статическим структурам данных"?
|
||||
**Q**: При построении стека (очереди) его размер не задается явно, почему же его относят к «статическим структурам данных»?
|
||||
|
||||
В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость `ArrayList` в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о "списках".
|
||||
В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость `ArrayList` в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о «списках».
|
||||
|
||||
**Q**: Если метод преобразования из прямого кода в дополнительный - это "сначала инвертировать, затем прибавить 1", то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией "сначала вычесть 1, затем инвертировать". Почему же дополнительный код также можно перевести в прямой тем же способом "сначала инвертировать, затем прибавить 1"?
|
||||
**Q**: Если метод преобразования из прямого кода в дополнительный - это «сначала инвертировать, затем прибавить 1», то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией «сначала вычесть 1, затем инвертировать». Почему же дополнительный код также можно перевести в прямой тем же способом «сначала инвертировать, затем прибавить 1»?
|
||||
|
||||
Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением "дополнения". Сначала дадим определение дополнения: если $a + b = c$ , то говорят, что $a$ является дополнением числа $b$ до $c$ ; аналогично, $b$ является дополнением числа $a$ до $c$ .
|
||||
Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением «дополнения». Сначала дадим определение дополнения: если $a + b = c$ , то говорят, что $a$ является дополнением числа $b$ до $c$. Аналогично, $b$ является дополнением числа $a$ до $c$ .
|
||||
|
||||
Для двоичного числа длины $n = 4$ со значением $0010$ , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом "сначала инвертировать, затем прибавить 1":
|
||||
Для двоичного числа длины $n = 4$ со значением $0010$ , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом «сначала инвертировать, затем прибавить 1»:
|
||||
|
||||
$$
|
||||
0010 \rightarrow 1101 \rightarrow 1110
|
||||
$$
|
||||
|
||||
Мы видим, что сумма прямого и дополнительного кодов равна $0010 + 1110 = 10000$ , то есть дополнительный код $1110$ является "дополнением" прямого кода $0010$ до $10000$ . **Это означает, что описанная выше операция "сначала инвертировать, затем прибавить 1" на самом деле вычисляет дополнение до $10000$ **.
|
||||
Мы видим, что сумма прямого и дополнительного кодов равна $0010 + 1110 = 10000$ , то есть дополнительный код $1110$ является «дополнением» прямого кода $0010$ до $10000$ . **Это означает, что описанная выше операция «сначала инвертировать, затем прибавить 1» на самом деле вычисляет дополнение до $10000$ **.
|
||||
|
||||
Тогда чему равно "дополнение" дополнительного кода $1110$ до $10000$ ? Мы снова можем получить его правилом "сначала инвертировать, затем прибавить 1":
|
||||
Тогда чему равно «дополнение» дополнительного кода $1110$ до $10000$ ? Мы снова можем получить его правилом «сначала инвертировать, затем прибавить 1»:
|
||||
|
||||
$$
|
||||
1110 \rightarrow 0001 \rightarrow 0010
|
||||
$$
|
||||
|
||||
Иначе говоря, прямой и дополнительный коды являются взаимными "дополнениями" друг друга до $10000$ , поэтому и "прямой код -> дополнительный код", и "дополнительный код -> прямой код" можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).
|
||||
Иначе говоря, прямой и дополнительный коды являются взаимными «дополнениями» друг друга до $10000$ , поэтому и «прямой код -> дополнительный код», и «дополнительный код -> прямой код» можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).
|
||||
|
||||
Разумеется, можно получить прямой код из дополнительного кода $1110$ и обратной операцией, то есть "сначала вычесть 1, затем инвертировать":
|
||||
Разумеется, можно получить прямой код из дополнительного кода $1110$ и обратной операцией, то есть «сначала вычесть 1, затем инвертировать»:
|
||||
|
||||
$$
|
||||
1110 \rightarrow 1101 \rightarrow 0010
|
||||
$$
|
||||
|
||||
В итоге и "сначала инвертировать, затем прибавить 1", и "сначала вычесть 1, затем инвертировать" - это два эквивалентных способа вычисления дополнения до $10000$ .
|
||||
В итоге и «сначала инвертировать, затем прибавить 1», и «сначала вычесть 1, затем инвертировать» - это два эквивалентных способа вычисления дополнения до $10000$ .
|
||||
|
||||
По сути операция "инвертировать" сама по себе вычисляет дополнение до $1111$ (потому что всегда выполняется `прямой код + обратный код = 1111` ); а дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до $10000$ .
|
||||
По сути операция «инвертировать» сама по себе вычисляет дополнение до $1111$ (потому что всегда выполняется `прямой код + обратный код = 1111` ). А дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до $10000$ .
|
||||
|
||||
Приведенный выше пример использовал $n = 4$ , но его можно обобщить на двоичные числа любой длины.
|
||||
|
||||
Reference in New Issue
Block a user