線形テーブル -- 逐次テーブル C 言語の実現
配列表からデータ構造の魅力を探る
数列テーブルは線形テーブルの一種であり、データ構造の中で最も単純かつ基本的な構造と言えます。多くの書籍では、これをデータ構造の魅力を説明する冒頭として使用しています。データ構造の開始として、この内容を注意深く学習して、データ構造に関する良い思考習慣を身に付ける必要があります。本質的に、データ構造はプロパティを定義し、後続の操作でこのプロパティを維持して特定の目的を達成することです。
アレイトーク
シーケンス テーブルは実際には「高度な」配列とみなすことができます。配列の性質はシーケンス テーブルの定義を満たしているためです。そのため、シーケンス テーブルについて説明する前に、配列の使用法とその欠点を見てみましょう。
配列名と配列先頭へのポインタ
配列名と配列ヘッダーへのポインターはよく混同されますが、厳密に言えば、この 2 つは異なります。
コードを見てみましょう:
#include <stdio.h>
int main() {
int array[] = {
1,2,3,4,5};
int *p = array;
printf("sizeof(array) = %d\n",sizeof(array));
printf("sizeof(p) = %d\n",sizeof(p));
return 0;
}
64 ビット マシンでの出力結果は次のとおりです。
sizeof(配列) = 20
sizeof(p)= 8
配列名は、配列が占有するメモリ量を含む配列全体を参照していることがわかります。配列名を使用して、配列内の要素の数を取得できます。配列の先頭へのポインタは本質的にポインタ変数であり、それが占めるバイト数は固定です。
配列全体を反復処理します
配列定義のスコープ内のみにある場合、配列の走査は非常に簡単で、必要なのは配列名だけです。
#include <stdio.h>
int main() {
int array[] = {
1,2,3,4,5};
int size = sizeof(array) / sizeof(int);
for (int i = 0; i < size;i++) {
printf("%d ",array[i]);
}
printf("\n");
return 0;
}
ただし、複数のスコープにわたって同じ機能を実現したい場合は、関数を使用し、パラメータを渡してそれを実現することがよくあります。しかし、C 言語では、渡すのは配列名ですが、コンパイラはそれをアドレスに変換して渡すため、配列の長さの情報が失われます。したがって、次のアプローチは明らかに間違っています。
#include <stdio.h>
void order(int array[]) {
int size = sizeof(array) / sizeof(int);
for (int i = 0; i < size;i++) {
printf("%d ",array[i]);
}
printf("\n");
}
int main() {
int array[] = {
1,2,3,4,5};
order(array);
return 0;
}
このようなエラーを回避するために、配列の長さをパラメータとして関数に渡すのが一般的です。次のように:
#include <stdio.h>
void order(int array[],int size) {
for (int i = 0; i < size;i++) {
printf("%d ",array[i]);
}
printf("\n");
}
int main() {
int array[] = {
1,2,3,4,5};
int size = sizeof(array) / sizeof(int);
order(array,size);
return 0;
}
ここから、配列の最初の欠点がすでに見つかります。配列の長さの情報は、パラメーターを渡す際に失われます。
配列の添字の範囲外アクセス
配列の添字が範囲外であることは、非常に一般的なエラーです。C 言語仕様では、これは未定義の動作です。各コンパイラはそれを異なる方法で処理します。しかし、一つ明らかなことは、そのような操作は非常に危険であるということです。
#include <stdio.h>
#define SIZE 5
int main() {
int array[SIZE] = {
1,2,3,4,5};
for (int i = 0;i < SIZE + 1;i++) {
printf("%d ",array[i]);
}
printf("\n");
return 0;
}
上記のコードから、最後にトラバーサルが実行されたとき、アクセスされた場所は array[5] であることが簡単にわかります。これは明らかに添え字が範囲外であることを意味し、このメモリ部分の具体的な値はわかりません。しかし、この問題はコンパイル中には発見されず、実行時に検出することさえ難しいため、配列の 2 番目の欠陥も明らかになります。配列添字の範囲外アクセスは未定義の動作であり、これを効果的に保護することはできません。
配列内のデータが無効です
手間を省くために大きな配列を一度に開いてしまうことが多いですが、実際の運用ではその一部しか使われていない(使うときは前から後ろに向かって使わないといけないと想定している)ため、未使用の配列空間にアクセスすることも危険です。実はこれはプログラミングスキルによってもたらされる隠れた危険なのです。
たとえば、今年の収入を日ごとに記録したいとします。年を単位として、大きな帳簿を取り出して直接記録します。今日が 4 月 1 日であるとします。このとき、5 月 1 日の収入を確認したいと考えています。現時点では、請求書にはこの日に対応する請求書が見つかりますが、この日は記録されていません。すべての照会は可能ですが、その情報は無効です。
配列の問題
前の 3 つの例を確認してください。通常のアレイの欠陥についてはすでに結論付けていますが、主な点は 2 つあります。
- 配列の長さ情報はパラメータの受け渡し時に失われます。
- 配列添字への範囲外のアクセスは未定義の動作であり、これを効果的に保護することはできません。
プログラミング スキルによってもたらされるもう 1 つの隠れた危険があります。
- 配列内の一部のデータが無効です
アレイの欠点を解決する
配列の欠点が明確にわかったので、適切な薬を処方してそれらを 1 つずつ解決できます。
上で述べたように、最初の 2 つの問題の解決策は、サイズフィールドを追加することです。これにより、配列の長さを知ることができるだけでなく、添え字も避けることができます。
プログラミングスキルによって引き起こされる隠れた危険は、長さフィールドを追加することで解決できます。このフィールドには、配列の最初の長さのデータが有効であり、それを超える部分は無効なデータとして記録されます。
上記の分析によれば、コードを書き始めることができます。
#include <stdio.h>
#define SIZE 100
int main() {
int array[SIZE] = {
0};
int size = sizeof(array) / sizeof(int); //记录array的长度
int length = 0; //记录array中有效数据的个数
return 0;
}
size フィールドとlengthフィールドを使用すると、配列の操作をより安全にし、配列の固有の欠点を補うことができます。
これまでのところ、実際に行ってきたことは、シーケンステーブルの構造定義。シーケンス テーブルは実際には配列を介して 2 つのフィールドを追加しており、配列よりも安全で信頼性が高いという目的を達成しています。
線形テーブル – 逐次テーブル
ここで、線形テーブルと順序テーブルの関係を明確にする必要があります。
線形テーブルは論理構造レベルの概念ですこれは、要素間の関係が 1 対 1 であることを意味します。
シーケンステーブルは物理構造レベルの概念ですこれは、要素が連続メモリ空間に格納され、配列がまさにそのような特性を持っていることを意味します。
線形テーブルの定義:
線形リストは順序付きリストとも呼ばれ、その各インスタンスは順序付けられた要素のコレクションです。各インスタンスの形式は(e 0 , e 1 , e 2 , ... en − 1 ) (e_0,e_1,e_2,...e_{n-1})( e0、e1、e2、。。。en − 1)
ここで、n は有限の自然数、ei e_ie私はは線形テーブルの要素、i は要素ei e_ie私はインデックス、n は線形テーブルの長さです。
インデックスが次のものであることがわかります。0始めるn-1終了。
シーケンステーブルの構造定義
シーケンシャル テーブルは実際には連続メモリ空間で実装された線形テーブルであり、配列が最良の選択です。実際、配列を分析するときに、シーケンス テーブルの構造定義をすでに記述しており、後続の操作を容易にするためにそれを構造にカプセル化します。
typedef struct SequentialList{
int *array; //数组
int size; //记录数组的长度
int length; //记录array中有效数据的个数
}SequentialList;
シーケンステーブルに関する操作
シーケンス テーブルの基本的な操作は次のとおりです。
- シーケンステーブルの作成
- 破壊シーケンス表
- ヌル操作
- 指定されたインデックスで要素を検索する
- 指定されたインデックスに要素を挿入します
- 指定されたインデックスによる要素を削除します
- 出力シーケンステーブル内のすべての要素を反復処理します。
等 次に、それらを 1 つずつ実装していきます。
シーケンステーブルの作成と破棄
C 言語には GC メカニズムがないため、これら 2 つの操作はペアで現れることが多く、一緒に実装します。
シーケンス テーブルを作成
する 作成する前に、初期化する必要がある数量とその値が何であるかを明確に知る必要があります。
配列の長さをカスタマイズできることは想像に難くありません。配列の長さはパラメーターを渡すことで制御する必要があり、それに対応するメモリ空間を開く必要があります。サイズ フィールドには配列の長さが記録され、これも決定できます。初期化された順序テーブルでは、有効要素数は0であり、長さの値が決定される。
SequentialList *createSequentialList(int _size) {
SequentialList *sl = (SequentialList*)malloc(sizeof(SequentialList));
sl->array = (int*)malloc(sizeof(int) * _size);
sl->size = _size;
sl->length = 0;
return sl;
}
破壊シーケンス テーブル
破壊の操作は非常に簡単で、メモリ リークを防ぐためにヒープ上のメモリを再利用します。
void destroySequentialList(SequentialList *sl) {
if (sl == NULL) return;
free(sl->array); //回收数组所占内存
free(sl); //回收顺序表对象的内存
return;
}
ヌル操作
null 判定演算は実際には補助的な演算であり、現在の有効データ数が 0 であるかどうかだけを知る必要があります。
int is_empty(SequentialList *sl) {
if (sl == NULL) return -1;
return sl->length == 0;
}
指定されたインデックスで要素を検索する
添字に従って要素を返すだけで十分ですが、データ インデックスの有効範囲は0 から length - 1 までである必要があることに注意してください。
int getElement(SequentialList *sl,int index) {
if (sl == NULL) return -1;
if (index < 0 || index > sl->length) return -1;
return sl->array[index];
}
指定されたインデックスに要素を挿入します
また、インデックスの正当な値にも注意する必要があります。インデックスの値は0 ~ lengthでなければなりません(挿入であるため、最後に挿入するのが合理的です)。同時に、挿入操作では、挿入用の空の位置ができるように、インデックスの後ろにあるすべての値を順番に 1 ビット後ろに戻す必要があります。そして、次のビットの順序は、移動を開始する最後のビットである必要があります。
int insert(SequentialList *sl,int index,int element) {
if (sl == NULL) return 0;
if (index < 0 || index > sl->length) return 0;
if (sl->length == sl->size) return 0;
for (int i = sl->length;i > index; i--) {
sl->array[i] = sl->array[i - 1];
}
sl->array[index] = element;
sl->length++;
return 1;
}
指定されたインデックスによる要素を削除します
この手法は挿入と似ていますが、今回は要素の移動方向がインデックスの最後の要素から開始され、一律に 1 ビット前方に移動する点が異なります。
int erase(SequentialList *sl,int index) {
if (sl == NULL) return 0;
if (index < 0 || index > sl->length) return 0;
for (int i = index + 1; i < sl->length; i++) {
sl->array[i - 1] = sl->array[i];
}
sl->length--;
return 1;
}
出力シーケンステーブル内のすべての要素を反復処理します。
void display(SequentialList *sl) {
if (sl == NULL) return;
if (is_empty(sl)) return;
for(int i = 0; i < sl->length; i++) {
printf("%d ",sl->array[i]);
}
printf("\n");
return;
}
シーケンステーブルの全体的な実装
#include <stdio.h>
#include <stdlib.h>
//顺序表结构定义
typedef struct SequentialList{
int *array; //数组
int size; //记录数组的长度
int length; //记录array中有效数据的个数
}SequentialList;
//创建顺序表
SequentialList *createSequentialList(int _size) {
SequentialList *sl = (SequentialList*)malloc(sizeof(SequentialList));
sl->array = (int*)malloc(sizeof(int) * _size);
sl->size = _size;
sl->length = 0;
return sl;
}
//销毁顺序表
void destroySequentialList(SequentialList *sl) {
if (sl == NULL) return;
free(sl->array); //回收数组所占内存
free(sl); //回收顺序表对象的内存
return;
}
//判空操作
int is_empty(SequentialList *sl) {
if (sl == NULL) return -1;
return sl->length == 0;
}
//按一个给定索引查找一个元素
int getElement(SequentialList *sl,int index) {
if (sl == NULL) return -1;
if (index < 0 || index > sl->length) return -1;
return sl->array[index];
}
//按一个给定索引插入一个元素
int insert(SequentialList *sl,int index,int element) {
if (sl == NULL) return 0;
if (index < 0 || index > sl->length) return 0;
if (sl->length == sl->size) return 0;
for (int i = sl->length;i > index; i--) {
sl->array[i] = sl->array[i - 1];
}
sl->array[index] = element;
sl->length++;
return 1;
}
//按一个给定索引删除一个元素
int erase(SequentialList *sl,int index) {
if (sl == NULL) return 0;
if (index < 0 || index > sl->length) return 0;
for (int i = index + 1; i < sl->length; i++) {
sl->array[i - 1] = sl->array[i];
}
sl->length--;
return 1;
}
//遍历输出顺序表中的全部元素
void display(SequentialList *sl) {
if (sl == NULL) return;
if (is_empty(sl)) return;
for(int i = 0; i < sl->length; i++) {
printf("%d ",sl->array[i]);
}
printf("\n");
return;
}
int main() {
SequentialList *sl = createSequentialList(100); //创建顺序表
for (int i = 0; i < 10; i++) {
if (!insert(sl,i,i)) //按一个给定索引插入一个元素
return -1; //插入失败
}
display(sl); //遍历输出顺序表中的全部元素
for (int i = 0;i < 5; i++) {
if (!erase(sl,0)) //按一个给定索引删除一个元素
return -1; //删除失败
}
display(sl); //遍历输出顺序表中的全部元素
for (int i = 0; i < sl->length; i++) {
printf("索引%d对应的数据为:%d\n",i,getElement(sl,i)); //按一个给定索引查找一个元素
}
destroySequentialList(sl); //销毁顺序表
return 0;
}
シーケンステーブルのメリットとデメリット
シーケンシャルテーブルは連続したメモリ空間で実装されるため、長所と短所は明らかです 長所
:高速な検索操作、間の添え字インデックス、計算量 O(1)
短所: 挿入および削除操作が遅い、配列全体を走査する必要がある、計算量 O(n)
追記
実際、C 言語の配列の欠陥は多くの言語で解消されています。Java や C# などの言語の配列には長さなどの情報があります。しかし、これはシーケンス テーブルの学習には影響しません。最終的には、データ構造は一種の思考ロジックです。特定の構造の意味とその実際の使用法を理解すれば、それを適切な場所で使用し、その本当の役割を果たすことができます。