データ構造は何ですか?
プログラム=データ構造+アルゴリズム
はい、上記の文は非常に古典的です。プログラムはデータ構造とアルゴリズムで構成されています。もちろん、データ構造とアルゴリズムは相互に補完し合うため、完全に独立して表示することはできません。ただし、この記事では、一般的に使用されるデータ構造に焦点を当てます。
データ構造は何ですか?
まず、データとは何ですか?データは、客観的な事柄を象徴的に表したものです。コンピューターサイエンスでは、コンピューターに入力してコンピュータープログラムで処理できるすべての記号を指します。では、なぜ「構造」という言葉を追加するのでしょうか。
データ要素はデータの基本単位であり、どのような問題でも、データ要素は独立して存在するわけではなく、常に関係があります。このデータ要素間の関係を構造と呼びます。
したがって、次の定義があります。
データ構造は、コンピューターがデータを格納および整理する方法です。データ構造は、相互に1つ以上の特定の関係を持つデータ要素のコレクションです。多くの場合、適切に選択されたデータ構造は、運用効率またはストレージ効率の向上につながる可能性があります。データ構造は、多くの場合、効率的な検索アルゴリズムとインデックス作成手法に関連付けられています。
簡単に言うと、データ構造は、データを整理、管理、および保存する方法です。理論的にはすべてのデータを混ぜたり、混ぜたり、食べ物なしで保存したりできますが、コンピューターは高効率を追求しています。データ構造を理解できれば、現在の問題シナリオにより適したデータ構造を見つけて、データ間の関係ストレージに関しては、計算時に適応アルゴリズムをより効率的に使用できるため、プログラムの実行効率が確実に向上します。
一般的に使用される4つのデータ構造は次のとおりです。
- セット:同じセットに属する関係のみで、他の関係はありません
- 線形構造:構造内のデータ要素間に1対1の関係があります
- ツリー構造:構造内のデータ要素間には1対多の関係があります
- グラフのような構造またはネットのような構造:グラフのような構造またはネットのような構造
論理構造とストレージ構造は何ですか?
データ要素間の論理関係は論理構造と呼ばれます。つまり、操作オブジェクトの数学的記述を定義します。しかし、それをコンピューターで表現する方法も知っておく必要があります。コンピューター内のデータ構造(イメージとも呼ばれます)の表現は、データの物理構造と呼ばれ、ストレージ構造とも呼ばれます。
データ要素の前の関係は、コンピュータで2つの異なる表現方法を持っています:シーケンシャルイメージと非シーケンシャルイメージ、そして2つの異なるストレージ構造がこれから得られます:シーケンシャルストレージ構造とシーケンシャルストレージ構造などのチェーンストレージ構造、複素数を表すz1 =3.0 - 2.3i
場合、データ要素間の論理関係は、メモリ内の要素の相対位置によって直接表すことができます。
チェーン構造は、ポインタを使用してデータ要素間の論理関係を表します。同様z1 =3.0 - 2.3i
に、アドレスである次の要素を最初100
に検索し、アドレスに従って実際のデータを検索します-2.3i
。
少し
コンピューターの情報を表す最小単位は、ビットと呼ばれる2進数のビットです。つまり、私たちは一般的に01010101010
このようなデータを目にします。コンピュータの最下層はあらゆる種類のトランジスタと回路基板であるため、データが何であれ、最下層の画像、音、合計さえも0
です1
。8つの回路がある場合、次に、各回路には独自の閉状態があり、2 ^ 8 ^8
の乗算があり、これは異なる信号です。2
256
ただし、一般に、負の数を表す必要があります。つまり、最上位ビットは符号ビットを0
表し、正の数を1
表し、負の数を表します。つまり、8ビットの最大値01111111
はです127
。
コンピューターの世界では、元のコード、逆コード、および補完コードの概念がさらにあることに注意してください。
- 元のコード:最初のビットを使用してシンボルを表し、残りのビットを使用して値を表します
- 補数コード:正の数の補数はそれ自体であり、負の数の補数は符号ビットが変更されないままであり、残りのビットが反転されることです。
- 2の補数:正の数の補数はそれ自体であり、負の数の補数はその補数+1に基づいています
元のコードの逆と補数が必要なのはなぜですか?
足し算と引き算は高頻度の演算であることがわかっています。人々はプラス記号とマイナス記号を直感的に確認でき、すぐに計算できます。ただし、コンピューターが異なる記号を区別すると、足し算と引き算はより複雑になります。正+正の数、正の数-正の数、正の数-負の数、負の数+負の数...など。したがって、同じ演算子(プラス演算子)を使用してすべての加算と減算の計算を解決したい場合があります。これにより、多くの複雑な回路とさまざまなシンボル変換のオーバーヘッドを削減でき、計算がより効率的になります。
操作に参加している次の負の数の結果も、補数の規則に準拠していることがわかります。
00100011 35
+ 11011101 -35
-------------------------
00000000 0
00100011 35
+ 11011011 -37
-------------------------
11111110 -2
もちろん、計算結果が桁数で表現できる範囲を超えるとオーバーフローしますので、正しく表現するにはさらに多くの桁が必要になります。
一般に、ビット演算を使用できる人は、より効率的であるため、ビット演算を使用するようにしてください。一般的なビット演算:
~
:ビット単位の否定&
:AND演算として|
:ビット単位のOR演算^
:ビット単位の排他的論理和<<
:符号付きの左シフト、たとえば35(00100011)
、左シフトはビット70(01000110)
、-35(11011101)
左シフトはビット-70(10111010)
>>
:符号付き右シフト、たとえば35(00100011)
、右シフトはビット17(00010001)
、-35(11011101)
左シフトはビット-18(11101110)
<<<
:符号なし左シフト、たとえば35(00100011)
、左シフトは70(01000110)
>>>
:符号なし右シフト、たとえば-35(11011101)
、右シフトは110(01101110)
x ^= y; y ^= x; x ^= y;
:両替s &= ~(1 << k)
:番目k
の位置0
ビット演算を使用する場所について話すことはより古典的です、そしてブルームフィルターを数えるために、あなたは詳細について参照することができます:http: //aphysia.cn/archives/cachebloomfilter
ブルームフィルターとは何ですか?
ブルームフィルター(Bloom Filter
)は、1970年にブルーム()によって提案されましたBurton Howard Bloom
。これは、実際には長いバイナリベクトルと一連のランダムハッシュマッピング関数で構成されています(端的に言えば、データの特徴を格納するバイナリ配列です)。コレクションに要素が存在するかどうかを判断するために使用できます。その利点は、クエリの効率が高く、スペースが小さいことです。欠点は、特定のエラーがあり、要素を削除するときに相互に影響を与える可能性があることです。
つまり、要素がコレクションに追加されると、複数の関数を介して、要素はビット配列内のポイントにhash
マップされ、に設定されます。k
1
重要なのは、データを異なるビットにハッシュできる複数のハッシュ関数があり、これらのビットがすべて1である場合にのみ、データがすでに存在していると判断できるということです。
3つのhash
関数があるとすると、異なる要素は3つのhash
関数hash
を3つの位置に使用します。
別の張さんがいるとすると、そこhash
にいると次の位置に移動hash
します。すべての位置は1
、張さんがすでに存在していると言えます。
それで、誤解の可能性はありますか?これは可能です。たとえば、現在はZhang San、Li Si、Wang Wu、CaiBaのみです。hash
マッピング値は次のとおりです。
Chen Liuは後で来ましたが、残念ながら、hash
その3つの関数のハッシュのビットがhash
他の要素によって変更されただけ1
で、すでに存在していると判断されましたが、実際にはChenLiuは存在しませんでした。
上記の状況は誤判断であり、ブルームフィルターは必然的に誤判断を引き起こします。ただし、ブルームフィルターでは、存在する要素は存在しない可能性がありますが、存在しない要素は存在してはならないという利点があります。、判断がないということは、そのうちの少なくとも1つhash
が正しくないことを意味するからです。
また、複数の要素がhash
一緒になる可能性があるが、1つのデータがコレクションから追い出され、そのマップされたビットをに設定する必要が0
あるためです。これは、データを削除することと同じです。このとき、他の要素が影響を受け、他の要素によってマップされた位置がに設定される場合があります0
。そのため、ブルームフィルターは取り外せません。
配列
線形表現は、最も一般的に使用される最も単純なデータ構造であり、次の特性を持つn個のデータ要素の有限シーケンスの線形表現です。
- ユニークな最初のデータ要素があります
- lastと呼ばれる一意のデータ要素があります
- 最初の要素を除いて、セット内の各要素には先行要素があります
- 最後の要素を除いて、コレクション内の各データ要素には後続要素があります
線形テーブルには次のものが含まれます。
- 配列:高速クエリ/更新、低速ルックアップ/削除
- リンクリスト
- 列
- スタック
配列は線形テーブルの一種であり、線形テーブルの順序は、線形テーブルのデータ要素が連続したアドレスを持つストレージユニットのグループに順番に格納されることを意味します。
Java
として示される:
int[] nums = new int[100];
int[] nums = {1,2,3,4,5};
Object[] Objects = new Object[100];
でC++
表されます:
int nums[100];
配列は線形構造であり、通常は最下層に連続したスペースがあり、同じタイプのデータを格納します。連続的なコンパクトな構造と自然なインデックスのサポートにより、クエリデータの効率は高くなります。
配列a
の最初の値がアドレス296
であり、その中のデータ型が1つ2
のがわかっているとすると、5番目の値を取得する場合は296+(5-1)*2 = 304
、O(1)
の時間計算量を取得できます。
更新の本質は、最初に要素を見つけて、更新を開始できるようにすることでもあります。
ただし、データを挿入する場合は、次の配列、要素の挿入など、次のデータを移動する必要があります。6
最悪の場合、すべての要素を移動することです。時間計算量はO(n)
要素を削除するには、次のデータを前面に移動する必要があります。最悪の時間計算量もO(n)
次のとおりです。
Javaコードは、配列の追加、削除、変更、および検査を実装します。
package datastruction;
import java.util.Arrays;
public class MyArray {
private int[] data;
private int elementCount;
private int length;
public MyArray(int max) {
length = max;
data = new int[max];
elementCount = 0;
}
public void add(int value) {
if (elementCount == length) {
length = 2 * length;
data = Arrays.copyOf(data, length);
}
data[elementCount] = value;
elementCount++;
}
public int find(int searchKey) {
int i;
for (i = 0; i < elementCount; i++) {
if (data[i] == searchKey)
break;
}
if (i == elementCount) {
return -1;
}
return i;
}
public boolean delete(int value) {
int i = find(value);
if (i == -1) {
return false;
}
for (int j = i; j < elementCount - 1; j++) {
data[j] = data[j + 1];
}
elementCount--;
return true;
}
public boolean update(int oldValue, int newValue) {
int i = find(oldValue);
if (i == -1) {
return false;
}
data[i] = newValue;
return true;
}
}
// 测试类
public class Test {
public static void main(String[] args) {
MyArray myArray = new MyArray(2);
myArray.add(1);
myArray.add(2);
myArray.add(3);
myArray.delete(2);
System.out.println(myArray);
}
}
リンクリスト
上記の例では、配列に連続したスペースが必要であることがわかります。スペースが大きい2
場合は、 3
th拡張する必要があります。それだけでなく、要素もコピーする必要があります。一部の削除および挿入操作により、より多くのデータ移動操作が発生します。
リンクリスト、つまり連鎖データ構造では、論理的に隣接するデータ要素が物理的な位置で隣接している必要がないため、シーケンシャルストレージ構造の欠点はありませんが、同時に直接検索も失われます。インデックスの添え字を介して。要素の利点。
重要:リンクリストはコンピューターのストレージに連続していませんが、前のノードは次のノードのポインター(アドレス)を格納し、後者のノードはアドレスから検出されます。
以下は、単一リンクリストの構造です。
通常、単一リンクリストの前にフロントノードを手動で設定します。これはヘッドノードとも呼ばれますが、これは絶対的なものではありません。
一般的なリンクリストの構造は、次のタイプに分けられます。
- 単一リンクリスト:リンクリスト内の各ノードには、次のノードへのポインターが1つだけあり、最後のノードはnullを指します。
- 二重リンクリスト:各ノードには2つのポインターがあり(便宜上、フロントポインターとバックポインターと呼びます)、それぞれ前のノードと次のノードを指し、最初のノードのフロントポインターは、を指し、後ろは
NULL
最後のノードのポインタが指すポインタNULL
- 循環リンクリスト:各ノードのポインタは次のノードを指し、最後のノードのポインタは最初のノードを指します(循環リンクリストですが、必要に応じてヘッドノードまたはテールノードを識別する必要があります。無限ループを避けてください)
- 複雑なリンクリスト:各リンクリストには、次のノードを指すバックポインターと、任意のノードを指すランダムポインターがあります。
リンクリスト操作の時間計算量:
- クエリ:
O(n)
、リンクリストをトラバースする必要があります - 挿入:
O(1)
、前後のポインタを変更します - 削除:
O(1)
、変更前後のポインタも同じです - 変更:クエリが必要ない場合、クエリが
O(1)
必要な場合は、O(n)
リンクリストの構造コードをどのように表現しますか?
以下は、単一リンクリスト構造のみを表しています。つまり、次のことをC++
意味します。
// 结点
typedef struct LNode{
// 数据
ElemType data;
// 下一个节点的指针
struct LNode *next;
}*Link,*Position;
// 链表
typedef struct{
// 头结点,尾节点
Link head,tail;
// 长度
int len;
}LinkList;
Java
コードは言う:
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
単純なリンクリストを自分で実装し、追加、削除、変更、およびチェックの機能を実装します。
class ListNode<T> {
T val;
ListNode next = null;
ListNode(T val) {
this.val = val;
}
}
public class MyList<T> {
private ListNode<T> head;
private ListNode<T> tail;
private int size;
public MyList() {
this.head = null;
this.tail = null;
this.size = 0;
}
public void add(T element) {
add(size, element);
}
public void add(int index, T element) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出链表长度范围");
}
ListNode current = new ListNode(element);
if (index == 0) {
if (head == null) {
head = current;
tail = current;
} else {
current.next = head;
head = current;
}
} else if (index == size) {
tail.next = current;
tail = current;
} else {
ListNode preNode = get(index - 1);
current.next = preNode.next;
preNode.next = current;
}
size++;
}
public ListNode get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表长度");
}
ListNode temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
public ListNode delete(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
ListNode node = null;
if (index == 0) {
node = head;
head = head.next;
} else if (index == size - 1) {
ListNode preNode = get(index - 1);
node = tail;
preNode.next = null;
tail = preNode;
} else {
ListNode pre = get(index - 1);
pre.next = pre.next.next;
node = pre.next;
}
size--;
return node;
}
public void update(int index, T element) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
ListNode node = get(index);
node.val = element;
}
public void display() {
ListNode temp = head;
while (temp != null) {
System.out.print(temp.val + " -> ");
temp = temp.next;
}
System.out.println("");
}
}
テストコードは次のとおりです。
public class Test {
public static void main(String[] args) {
MyList myList = new MyList();
myList.add(1);
myList.add(2);
// 1->2
myList.display();
// 1
System.out.println(myList.get(0).val);
myList.update(1,3);
// 1->3
myList.display();
myList.add(4);
// 1->3->4
myList.display();
myList.delete(1);
// 1->4
myList.display();
}
}
出力結果:
1 -> 2 ->
1
1 -> 3 ->
1 -> 3 -> 4 ->
1 -> 4 ->
単一リンクリストの検索と更新は比較的簡単です。新しいノードを挿入する特定のプロセスを見てみましょう(ここでは中央の位置での挿入のみが示され、頭と尾の挿入は比較的簡単です)。
中間ノードを削除するにはどうすればよいですか?具体的なプロセスは次のとおりです。
気になるかもしれませんが、a5
ノードにはポインタがないので、どこに行きますか?
プログラムの場合Java
、ガベージコレクターはそのような参照されていないノードを収集し、メモリのこの部分をリサイクルするのに役立ちますが、ガベージコレクションを高速化するには、通常、不要なノードを空にする必要がありますnode = null
。プログラムではC++
、手動でリサイクルする必要があります。そうしないと、メモリリークなどの問題が発生しやすくなります。
ここでは、複雑なリンクリストの操作について簡単に説明します。後で、リンクリストのデータ構造と一般的なアルゴリズムを個別に共有します。この記事では、主にデータ構造の全体像について説明します。
テーブルをスキップ
上記のように、リンクリストを検索する場合は非常に面倒です。このノードが最後にある場合は、すべてのノードをトラバースして検索する必要があります。検索効率が低すぎます。良い方法はありますか?
問題よりも解決策は常にありますが多快好省
、絶対的な「」のようなものはありません。与えるものがあります。コンピュータの世界は哲学的な味わいに満ちています。検索効率に問題があるため、リンクリストを並べ替えた方がよいでしょう。ソートされたリンクリストは、ヘッドノードとテールノード、および中央の範囲しか認識していませんが、中央のノードを見つけるには、古い方法でトラバーサルを実行する必要があります。中間ノードを保存するとどうなりますか?保存してください。データが前半または後半にあることがわかります。たとえば、を見つける7
には、中間ノードから開始する必要があります。検索する場合4
は最初から開始する必要があり、最悪の場合は中間ノードに到達した場合は検索を停止します。
ただし、リンクリストは非常に長く、前後の2つの部分でしか検索できないため、問題はまだ完全には解決されていません。原則に戻ることをお勧め空间和时间,我们选择时间,那就要舍弃一部分空间
します。各ノードにポインターを追加すると、2層のポインターが作成されます(注:ノードのコピーは1つだけで、すべて同じノードです。外観については、実際には同じノードである2つのコピーを作成しました。2と5の両方を指す2つのポインター(たとえば1)があります):
ポインタの2つのレイヤー、問題はまだ存在します。その後、レイヤーを追加し続けます。たとえば、2つのノードごとに1つのレイヤーを追加します。
これがスキップテーブルです。スキップテーブルの定義は次のとおりです。
スキップリスト(スキップリストのフルネームであるSkipList)は、順序付けられた要素シーケンスの高速検索と検索に使用されるデータ構造です。スキップリストはランダム化されたデータ構造であり、基本的にはバイナリ検索を実行できる順序付けられたリンクリストです。 。スキップリストは、元の順序付きリンクリストにマルチレベルのインデックスを追加し、そのインデックスを使用して高速検索を実現します。テーブルをスキップすると、検索のパフォーマンスだけでなく、挿入および削除操作のパフォーマンスも向上します。パフォーマンスは赤黒木やAVL木に匹敵しますが、スキップテーブルの原理は非常に単純であり、実装は赤黒木よりもはるかに単純です。
主な原理は時間と空間を交換することであり、これにより二分探索のほぼ効率を達成できます。実際、2層ごとに追加すると仮定すると、消費される空間は1 + 2 + 4 + ... + n = 2n-1
ほぼ2倍になります。それは本の目次、第1レベルのディレクトリ、第2レベル、第3レベルのように見えると思いますか...
スキップテーブルにデータを挿入し続けると、特定のセグメントにノードが多すぎる場合があります。このとき、インデックスを動的に更新する必要があります。データを挿入するだけでなく、データも挿入する必要があります。クエリの効率を確保するために、前のレイヤーのリンクリストに追加します。
redis
zset
これを実現するためにスキップテーブルが使用されredis
、ランダムアルゴリズムを使用してレベルが計算され、ノードごとにインデックスのレベル数が計算されます。比較は絶対に保証されませんが、効率は基本的に保証され、より効率的です。それらのバランスの取れた木や赤黒木よりも。アルゴリズムはより単純です。
スタック
スタックは、クラスが具体化さJava
れるデータ構造です。Stack
その本質は、バケツのように、連続してしか配置できないファーストイン、ラストアウトであり、取り出すと、最上位のデータのみを連続的に取り出すことができます。一番下のデータを取り出したい場合は、上のデータを取り出したときにのみできます。もちろん、そのような必要がある場合は、通常、双方向キューを使用します。
以下は、スタックの特性のデモンストレーションです。
スタックの最下層は何に使用されますか?実際、リンクリストまたは配列を使用できますがJDK
、基になるスタックは配列で実装されます。カプセル化後はAPI
、最後の要素のみを操作できます。スタックは、再帰関数を実装するためによく使用されます。Java
内部のスタックまたはその他のコレクション実装分析を理解したい場合は、次の一連の記事を参照してください:http: //aphysia.cn/categories/collection
要素がスタックに追加され(プッシュされ)、要素がスタックから取り出され、スタックの一番上の要素がスタックに配置された最後の要素になります。
配列を使用して単純なスタックを実装します(これは参照テスト専用であり、実際にはスレッドセーフやその他の問題が発生することに注意してください)。
import java.util.Arrays;
public class MyStack<T> {
private T[] data;
private int length = 2;
private int maxIndex;
public MyStack() {
data = (T[]) new Object[length];
maxIndex = -1;
}
public void push(T element) {
if (isFull()) {
length = 2 * length;
data = Arrays.copyOf(data, length);
}
data[maxIndex + 1] = element;
maxIndex++;
}
public T pop() {
if (isEmpty()) {
throw new IndexOutOfBoundsException("栈内没有数据");
} else {
T[] newdata = (T[]) new Object[data.length - 1];
for (int i = 0; i < data.length - 1; i++) {
newdata[i] = data[i];
}
T element = data[maxIndex];
maxIndex--;
data = newdata;
return element;
}
}
private boolean isFull() {
return data.length - 1 == maxIndex;
}
public boolean isEmpty() {
return maxIndex == -1;
}
public void display() {
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]+" ");
}
System.out.println("");
}
}
テストコード:
public class MyStackTest {
public static void main(String[] args) {
MyStack<Integer> myStack = new MyStack<>();
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.display();
System.out.println(myStack.pop());
myStack.display();
}
}
予想どおり、出力は次のとおりです。
1 2 3 4
4
1 2 3
スタックの特徴は後入れ先出しですが、前のデータをランダムに取り出す必要がある場合、効率は比較的低くなり、空にする必要がありますJava
。
列
目の前に先入れ先出しのデータ構造があるため、先入れ先出しのデータ構造も必要です。流行中、キュー内の全員がテストされたと推定されます。核酸。キューが長く、最初の行が最初にテストされ、最後の行がテストされます。テスト、誰もがこれを知っています。
キューは特殊な種類の線形テーブルです。特殊な機能は、テーブルの前面での削除操作とテーブルの背面での挿入操作のみを許可することです。スタックと同様に、キューは線形制約テーブルの対象となる操作です。挿入操作を実行する端はキューの末尾と呼ばれ、削除操作を実行する端はキューの先頭と呼ばれます。
キューは、ファーストイン、ファーストアウトによって特徴付けられます。以下に例を示します。
一般的に言えば、先入れ先出し(FIFO
)、フルネームについて話す限りFirst In First Out
、キューを思い浮かべますが、キューの先頭から要素を取得して取得できるキューが必要な場合は、キューの末尾の要素の場合、特別なキュー(双方向キュー)を使用する必要があります。)、双方向キューは通常、二重にリンクされたリストを使用して実装する方が簡単です。
以下にJava
、単純な一方向キューを実装します。
class Node<T> {
public T data;
public Node next;
public Node(T data) {
this.data = data;
}
}
public class MyQueue<T> {
private Node<T> head;
private Node<T> rear;
private int size;
public MyQueue() {
size = 0;
}
public void pushBack(T element) {
Node newNode = new Node(element);
if (isEmpty()) {
head = newNode;
} else {
rear.next = newNode;
}
rear = newNode;
size++;
}
public boolean isEmpty() {
return head == null;
}
public T popFront() {
if (isEmpty()) {
throw new NullPointerException("队列没有数据");
} else {
Node<T> node = head;
head = head.next;
size--;
return node.data;
}
}
public void dispaly() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data +" -> ");
temp = temp.next;
}
System.out.println("");
}
}
テストコードは次のとおりです。
public class MyStackTest {
public static void main(String[] args) {
MyStack<Integer> myStack = new MyStack<>();
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.display();
System.out.println(myStack.pop());
myStack.display();
}
}
演算結果:
1 -> 2 -> 3 ->
1
2 -> 3 ->
2
3 ->
一般的に使用されるキュータイプは次のとおりです。
-
一方向キュー:つまり、先入れ先出しの通常のキューと呼ばれるものです。
-
双方向キュー:さまざまな方向からキューに出入りできます
-
優先キュー:内部は自動的にソートされ、キューは特定の順序でキューに入れられます
-
キューのブロック:要素がキューから取得されると、要素がない場合はキューがブロックされます。同様に、キューがいっぱいになると、要素をキューに入れることもブロックされます。
-
循環キュー:循環リンクリストとして理解できますが、無限ループを防ぐためにヘッドノードとテールノードを識別する必要があり、テールノード
next
はヘッドノードを指します。
キューは通常、順序付けが必要なデータの保存やタスクの保存に使用できます。ツリーレベルのトラバーサルでは、キューを使用してそれらを解決できます。通常、幅優先探索はキューを使用して解決できます。
ハッシュ表
以前のデータ構造は、検索時に一般的に使用され=
、または、!=
半分または他の範囲クエリで検索するときに使用される場合があります<
。>
理想的には、比較せずに特定の位置を直接見つけることを望んでいます。要素はインデックスで取得できます。したがって、格納するデータを配列のインデックスと一致させ、それが1対1の関係である場合、要素の位置をすばやく見つけることはできませんか?
f(k)
関数を介して対応する位置を見つけることができる限りk
、この関数f(k)
はhash
関数です。これはマッピング関係を表しますが、値が異なる場合は、同じ値(同じhash
アドレス)にマッピングされる可能性があります。つまりf(k1) = f(k2)
、この現象をまたはと呼び冲突
ます碰撞
。
hash
テーブルは次のように定義されています。
ハッシュテーブル(ハッシュテーブルとも呼ばれます)は、キーに従ってメモリの保存場所に直接アクセスするデータ構造です。つまり、クエリ対象のデータをテーブル内の場所にマップするキー値の関数を計算することでレコードにアクセスします。これにより、ルックアップが高速化されます。このマッピング関数はハッシュ関数と呼ばれ、レコードの配列はハッシュテーブルと呼ばれます。
一般的に使用されるhash
関数は次のとおりです。
H(key) = key
直接アドレス指定方法:キーワードまたはキーワードの線形関数の値を、またはなどのハッシュ関数として取り出します。H(key) = a * key + b
- 数値解析方法:可能なすべての値について、キーワードの数桁を使用してハッシュアドレスを形成します
- 二乗法:キーワードの二乗の後の真ん中の数字をハッシュアドレスとして使用します
- 折りたたみ方法:キーワードを同じ桁数の複数の部分に分割し(最後の部分の桁数は異なる場合があります)、これらの部分の重ね合わせの合計(切り捨て)をハッシュアドレスとして使用します。
- 除算の余り:キーワードをハッシュテーブルテーブル
m
の長さ以下の数値で除算した余りをp
ハッシュアドレスとします。つまり、hash(k)=k mod p
、。p< =m
キーワードを直接モジュロにするだけでなく、フォールディング法やスクエア法などの演算の後にモジュロをとることもできます。正しいp
選択は非常に重要です。一般的に、素数または素数が使用されm
ます。p
選択が適切でない場合、競合が発生しやすくなります。 - 乱数法:キーワードのランダム関数値をハッシュアドレスとします。
ただし、これらの方法はいずれもハッシュの衝突を回避することはできず、意識的に減らすことしかできません。hash
では、紛争に対処する方法は何ですか?
- オープンアドレス方式:
hash
計算後、その場所にすでにデータがある場合、アドレス+1
、つまり振り返ってみると、空の場所を見つけることがわかります。 - 再
hash
方法:ハッシュの競合が発生した後、別のhash
関数を使用して極を再計算し、空のhash
アドレスを見つけることができます。空のアドレスがある場合は、hash
関数を重ね合わせることができます。 - チェーンアドレス方式:すべての
hash
値は同じであり、リンクはリンクリストになり、配列の後ろにぶら下がっています。 - 共通のオーバーフロー領域を確立します。共通ではありません。つまり、すべての要素
hash
がテーブル内の要素と競合する場合、オーバーフローテーブルとも呼ばれる別のテーブルを取得します。
Java
内部では、チェーンアドレス方式が使用されます。
ただし、hash
競合が深刻な場合は、リンクリストが比較的長くなります。クエリを実行する場合は、次のリンクリストをトラバースする必要があるため、JDK
バージョンが最適化されます。リンクリストの長さがしきい値を超えると、赤黒木。赤黒木には、クエリの効率に影響を与えるリンクリストへの縮退を回避するために、サブツリーのバランスをとる特定のルールがあります。
しかし、配列が小さすぎてより多くのデータが配置された場合はどうなるでしょうか。競合が再現される可能性はますます高くなります。実際、この時点で、拡張メカニズムがトリガーされ、配列が2
2倍、hash
前のデータが別の配列にハッシュされます。
hash
このテーブルの利点は、ルックアップ速度が速いことですが、再トリガーが常にトリガーされるhash
と、応答速度も遅くなります。また、範囲クエリが必要な場合は、hash
テーブルは適切な選択ではありません。
木
配列とリンクリストはどちらも線形構造ですが、ここで紹介するツリーは非線形構造です。実際には、ツリーはピラミッド構造であり、データ構造内のツリーは上部のルートノードと呼ばれます。
ツリー構造をどのように定義しますか?
ツリーはデータ構造であり、n個(n≥1)の有限ノードで構成され、階層関係のあるセットを形成します。逆さまの木のように見えるので「木」と呼ばれます。つまり、根が上に、葉が下になっています。次の特徴があります。
各ノードには0個以上の子ノードがあります。親ノードのないノードはルートノードと呼ばれます。すべての非ルートノードには1つだけの親ノードがあります。ルートノードを除いて、各子ノードは複数のばらばらの子に分割できます。ツリー。(百度百科事典)
以下は、ツリーの基本的な用語です(清華大学データ構造C
言語版から)。
- ノードの次数:ノードに含まれるサブツリーの数は、ノードの次数と呼ばれます。
- ツリーの次数:ツリーでは、最大のノード次数はツリーの次数と呼ばれます。
- リーフノードまたはターミナルノード:次数がゼロのノード。
- 非終端ノードまたは分岐ノード:次数がゼロではないノード。
- 親ノードまたは親ノード:ノードに子ノードが含まれている場合、そのノードはその子ノードの親ノードと呼ばれます。
- 子ノードまたは子ノード:ノードに含まれるサブツリーのルートノードは、ノードの子ノードと呼ばれます。
- 兄弟ノード:同じ親ノードを持つノードは兄弟ノードと呼ばれます。
- ノードのレベル:ルートの定義から始めて、ルートは最初の
1
レイヤーであり、ルートの子ノードは最初の2
レイヤーであり、以下同様です。 - 深さ:どのノード
n
でn
も、深さはルートからnまでの一意のパスの長さであり、ルートの深さは0
;です。 - 高さ:どのノード
n
でn
も、高さはn
葉から葉までの最長のパスの長さであり、すべての葉の高さは0
;です。 - いとこノード:親ノードが同じレイヤーにあるノードは、お互いのいとこです。
- ノードの祖先:ルートからノードへのブランチ上のすべてのノード。
- 子孫:ノードをルートとするサブツリー内のノードは、ノードの子孫と呼ばれます。
- 順序付けられたツリー:樹種のノードのサブツリーは、左から右に順序付けられていると見なされます(交換できません)。その場合、ツリーは順序付けられたツリーと呼ばれる必要があります。それ以外の場合は、順序付けられていないツリーです。
- 最初の子:順序付けられたツリーの左端のサブツリーのルートは、最初の子と呼ばれます
- 最後の子:順序付けられたツリーの右端のサブツリーのルートは、最後の子と呼ばれます
m
森:(m>=0
)ばらばらの木の集まりは森と呼ばれます。
実際、ツリーは最も一般的にバイナリツリーを使用します。
二分木の特徴は、各ノードに最大2つのサブツリーがあり、サブツリーが左右に分割され、左右の子ノードの順序を任意に逆にすることができないことです。
二分木Java
は次のように表されます。
public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;
TreeLinkNode(int val) {
this.val = val;
}
}
完全な二分木:深さがkで2 <sup> k </sup>-1ノードの二分木は完全な二分木と呼ばれます
完全な二分木:n個のノードを持つ深さkの二分木。各ノードが深さkの完全な二分木で1からnまでの番号が付けられたノードに対応する場合に限り、完全な二分木と呼ばれます。
一般的な二分木のトラバーサルにはいくつかのタイプがあります。
- プレオーダートラバーサル:オーダールートノード->左の子ノード->右の子ノードをトラバースします
- インオーダートラバーサル:トラバーサル順序左子ノード->ルートノード->右子ノード
- ポストオーダートラバーサル:トラバースオーダー左子ノード->右子ノード->ルートノード
- 幅/レベルトラバーサル:上から下へのトラバーサル、レイヤーごと
混沌とした二分木だと、検索や検索の効率が比較的低くなり、混沌としたリンクリストと変わらないのに、なぜもっと複雑な構造に悩まされるのでしょうか。
実際、二分木は並べ替えや検索に使用できます。二分木には厳密な左右のサブツリーがあるため、ルートノード、左の子ノード、および右の子ノードのサイズを定義できます。したがって、二分探索木があります。
バイナリ検索ツリー(また、バイナリ検索ツリー、バイナリソートツリー)これは、空のツリー、または次のプロパティを持つバイナリツリーのいずれかです。左側のサブツリーが空でない場合は、左側のサブツリー上のすべてのノードの値はそのルートノードの値よりも小さい;その右のサブツリーが空でない場合、右のサブツリー上のすべてのノードの値はそのルートノードの値よりも大きい;その左、右のサブツリーも二分探索木である、それぞれ。古典的なデータ構造として、バイナリ検索ツリーは、リンクリストの高速挿入と削除の特性を備えているだけでなく、高速配列検索の利点も備えているため、広く使用されています。たとえば、ファイルシステムやデータベースシステムは一般的にこれを使用します。一種のツリー。効率的なソートおよび取得操作のためのデータ構造。
サンプルの二分探索木は次のとおりです。
たとえば、上記のツリーを見つける必要がある場合は、4
から5
始め4
て5
、左側のサブツリーに移動し、それを検索し3
、右側のサブツリーに移動して、それ、つまりノードのツリーを検索します。つまり、検索時間のみです。ノードを想定したレイヤーの数、つまり。4
3
4
7
3
n
log(n+1)
ツリーが適切に維持されている場合、クエリの効率は高くなりますが、ツリーが適切に維持されていない場合、リンクリストに簡単に縮退し、クエリの効率も低下します。たとえば、次のようになります。
クエリに適したバイナリツリーは、バランスの取れた、またはほぼバランスの取れたバイナリツリーである必要があります。バランスの取れたバイナリツリーとは次のとおりです。
平衡二分探索木のノードの左右のサブツリーの高さは、最大で1だけ異なります。平衡二分木はAVL木とも呼ばれます。
データの挿入や削除などを行った後も、バイナリツリーがバランスの取れたバイナリツリーであるようにするには、ノードを調整する必要があります。これは、拡張されないさまざまな回転調整を伴うバランスプロセスとも呼ばれます。とりあえずここに。
ただし、多数の更新、削除が必要であり、樹種のバランスをとるためのさまざまな調整が多くのパフォーマンスを犠牲にする必要がある場合、この問題を解決するために、一部のボスは赤黒木を提案しました。
赤黒木(赤黒木)は、自己平衡二分探索木であり、コンピューターサイエンスで使用されるデータ構造であり、通常、連想配列を実装するために使用されます。[1]
赤黒木は1972年に[RudolfBayer]( https://baike.baidu.com/item/Rudolf Bayer / 3014716)によって発明され、対称バイナリBツリーと呼ばれていました。その後、1978年にレオJ.ギバスとロバートセッジウィックによって現在の「赤黒木」に変更されました。[2]
赤黒木は特殊なAVLツリー(平衡二分木)であり、挿入および削除操作中の特定の操作を通じて二分探索木のバランスを維持し、高い検索パフォーマンスを実現します。
赤黒木には次の特徴があります。
-
プロパティ1.ノードは赤または黒です。
-
プロパティ2。ルートノードは黒です。
-
プロパティ3。すべての葉は黒です。(リーフはNILノードです)
-
プロパティ4。各赤いノードの両方の子は黒です。(各リーフからルートまでのすべてのパスに2つの連続した赤いノードがあってはなりません)
-
プロパティ5.任意のノードからその各リーフへのすべてのパスには、同じ数の黒いノードが含まれています。
赤黒木の調整を通常の平衡二分木の調整ほど難しく頻繁にしないのは、これらの特性です。つまり、特定の基準を満たし、バランシングプロセスの混乱と頻度を減らすために、ルールが追加されます。
上記のハッシュテーブルの実装は、Java
まさに赤黒木を応用したものであり、hash
競合が多い場合は、リンクリストを赤黒木に変換します。
上記はすべて二分木ですが、マルチフォークの木をリッピングする必要があります。なぜですか?バイナリツリーのさまざまな検索ツリーですが、赤黒木はすでに非常に優れていますが、ディスクと対話するとき、それらのほとんどはデータストレージにあります。ディスクのIOははるかに遅いため、IO係数を考慮する必要があります。メモリより。インデックスツリーのレベルが数万の場合、ディスク読み取りの数が多すぎます。Bツリーはディスクストレージに適しています。
970年、R.BayerとE.mccreightは、外部検索に適したツリーを提案しました。これは、Bツリー(またはBツリー、B_tree)と呼ばれるバランスの取れたマルチフォークツリーです。
次数mの平衡木は、平衡m-way探索木です。これは、空のツリー、または次のプロパティを満たすツリーのいずれかです。
1.ルートノードには少なくとも2つの子があります。
2.各非ルートノードに含まれるキーワードの数jは、次の条件を満たす。m / 2- 1 <= j <= m-1;
3.ルートノード(リーフノードを除く)を除くすべてのノードの次数は、キーワードの総数に1を加えたものであるため、内部サブツリーの数kは次の条件を満たす。m/ 2 <= k <= m;
4.すべてのリーフノードは同じレイヤーにあります。
各ノードはもう少し多くのデータを格納します。検索する場合、メモリ内の操作はディスク上の操作よりもはるかに高速であり、b
ツリーはディスクIOの数を減らすことができます。Bツリー:
また、各ノードdata
が非常に大きくなる可能性があるため、各ページで検出されるデータが非常に少なくなり、IOクエリの数が自然に増加します。その場合、リーフノードにのみデータを格納することもできます。
B +ツリーはBツリーの変形であり、B +ツリーのリーフノードには対応するレコードのキーワードとアドレスが格納され、リーフノードの上のレイヤーはインデックスとして使用されます。次数mのB+ツリーは次のように定義されます。
(1)各ノードには最大m個の子があります。
(2)ルートノードを除いて、各ノードには少なくとも[m / 2]の子があり、ルートノードには少なくとも2つの子があります。
(3)k個の子を持つノードには、k個のキーワードが必要です。
一般に、b +ツリーのリーフノードはリンクリストで接続されており、トラバーサルとレンジトラバーサルに便利です。
これがツリーです。ツリーと比較すると、b+
ツリーには次の利点があります。b+
B树
b+
ツリーの中間ノードはデータを保存せず、各IOクエリはスクワットツリーであるより多くのインデックスを見つけることができます。- 範囲検索の場合、
b+
ツリーはリーフノードのリンクリストをトラバースするだけで済みますb
が、ツリーはルートノードからリーフノードまで開始する必要があります。
上記のツリーに加えて、実際には一種のツリーがあります。N個のリーフノードHuffman
としてN個の重みが与えられ、バイナリツリーを構築します。ツリーの重み付きパスの長さが最小に達した場合、そのようなバイナリツリーは最適なバイナリツリーと呼ばれます。 、ハフマンツリーとも呼ばれます。ハフマンツリーは、重み付きパスの長さが最も短いツリーであり、重みが大きいノードはルートに近くなります。
データ内の各文字の頻度が異なるため、一般的に圧縮に使用されます。文字の頻度が高いほど、保存に使用するコードが短くなり、圧縮の目的を達成できます。このコードはどこから来たのですか?
文字がであると仮定するとhello
、エンコーディングは次のようになります(エンコーディングの大まかなプロトタイプ、高周波文字、エンコーディングは短い)、エンコーディングは01
ルートノードから現在の文字へのパスの文字列です。
異なる重みをエンコードすることにより、ハフマンツリーは効果的に圧縮されます。
ヒープ
ヒープは実際には一種のバイナリツリーです。ヒープは完全なバイナリツリーである必要があります。完全なバイナリツリーは次のとおりです。最後のレイヤーを除いて、他のレイヤーのノード数がいっぱいで、最後のレイヤーのノードが集中しています。左の連続位置にあります。
ヒープには別の要件があります。ヒープ内の各ノードの値は、その左右の子ノードの値以上(または以下)である必要があります。
ヒープには主に2つのタイプがあります。
- ビッグトップヒープ:各ノードはそのサブツリーノード以上です(ヒープトップは最大値です)
- 小さなトップヒープ:各ノードはそのサブツリーノード以下です(ヒープトップは最小値です)
一般に、次の小さなトップヒープなど、ヒープを表すために配列を使用します。
配列内の親子ノードと左右のノードの関係は次のとおりです。
i
ノードの親parent = floor((i-1)/2)
(切り捨て)i
ノードの左の子2 * i +1
i
ノードの右の子2 * i + 2
データが保存されるため、挿入や削除などの操作が必要です。ヒープへの挿入と削除には、ヒープの調整が含まれます。調整後、その定義を再満たすことができます。この調整プロセスは、ヒープ化と呼ばれます。
小さなトップヒープを例にとると、調整は主に次のことを確認するためのものです。
- または完全な二分木
- ヒープ内の各ノードは、その左右の子ノード以下です
小さなトップスタックの場合、調整は次のとおりです。小さな要素が浮き上がり、大きな要素が沈みます。これは、継続的な交換のプロセスです。
ヒープは通常TOP K
、問題を解決するために使用することも、前述の優先キューを使用することもできます。
写真
ついに地図の説明にたどり着きました。地図は実際には2次元平面です。以前に掃海について書きました。掃海のブロック領域全体が実際には地図に関連していると言えます。グラフは非線形のデータ構造であり、主にエッジと頂点で構成されます。
同時に、グラフは有向グラフと無向グラフに分けられます。上記は無向グラフです。エッジは方向を示しておらず、両者の関係を示しているだけですが、有向グラフは次のようになっています。 :
各頂点が場所であり、各エッジがパスである場合、これはマップネットワークであるため、最短距離を解決するためにグラフがよく使用されます。グラフに関連する概念を見てみましょう。
- 頂点:グラフの最も基本的な単位であるノード
- エッジ:頂点間の関係
- 隣接する頂点:エッジによって直接関連する頂点
- 度:頂点が直接接続されている隣接する頂点の数
- 重量:エッジの重量
一般に、グラフを表すにはいくつかの方法があります。
- 2次元配列で表される隣接行列は、接続性が1、非接続性が0です。もちろん、パス長を表す場合は、パス長を表す
0
数値を大きくして、表示することができ-1
ます。切断。
下の図では、0と1、2が接続されており、0行目の1列目と2列目が1であり、接続されていることを示しています。もう1つのポイント:頂点自体は0でマークされており、接続されていないことを示していますが、場合によっては接続状態と見なすことができます。
- 隣接リスト
隣接リストは、ツリーの子チェーン表現に似たストレージ方式であり、順次割り当てとチェーン割り当てを組み合わせたストレージ構造です。ヘッダーノードに対応する頂点に隣接する頂点がある場合、隣接する頂点は、ヘッダーノードが指す単一リンクリストに順番に格納されます。
無向グラフの場合、ストレージに隣接リストを使用すると、データの冗長性も発生します。ヘッダーノードAが指すリンクリストにCを指すテーブルノードがある場合、ヘッダーノードCが指すリンクリストもAを指すテーブルノード。
グラフのトラバーサルは、通常、幅優先トラバーサルと深さ優先トラバーサルに分けられます。幅優先トラバーサルとは、現在の頂点に直接関連する頂点の優先トラバーサルを指し、通常、キューによって実装されます。深さ優先探索とは、一方向に一方向に進むことで、それ以上進むことはできません。つまり、南の壁にぶつかったり、振り返ったりしないことを意味します。通常、再帰的に実行されます。
最小パスの計算に加えて、別の概念があります。最小スパニングツリーです。
n個のノードを持つ連結グラフのスパニングツリーは、元のグラフの最小の連結サブグラフであり、元のグラフのn個のノードすべてを含み、グラフの連結を維持するエッジが最も少なくなります。最小全域木は、クラスカルアルゴリズムまたはプリムアルゴリズムによって計算できます。
グラフは平面上の点であるということわざがありますが、その点の1つをピックアップすると、他の頂点をまとめることができるエッジが最小の重みを取り、最小全域木である冗長なエッジを削除します。
もちろん、最小スパニングツリーは必ずしも一意ではなく、複数の結果が生じる可能性があります。
Qin Huai @ Viewpoint
これらの基本的なデータ構造を知ることは、コードまたはデータモデリングを記述し、より適切なものを選択できる場合に最も役立ちます。コンピューターは人々に役立ち、コードも同様です。すべての種類のデータ構造を一度に習得することはできませんが、新世代の革命的な変化がない限り、基本的なことはそれほど変わりません。
プログラムはデータ構造とアルゴリズムで構成されています。データ構造は基礎のようなもので、「データ構造C言語」バージョンの文で終わります。
「良い」プログラムを書くためには、「データ構造」の規律と発展の背景である、処理されるオブジェクトの特性と処理されるオブジェクト間の関係を分析する必要があります。
【作者の簡単な紹介】:
Qin Huai、パブリックアカウント[ Qin Huai Grocery Store ]の作者、個人ウェブサイト:http://aphysia.cn、テクノロジーの道はかつてなく、山は高く、川は長くても、遅くても終わりはありません。