Array Indexing from http://acm.nudt.edu.cn/~twcourse/ArrayIndexing.html
「索引」可說是電腦的奇技!一個元素存放到陣列之後,不論是在陣列的哪個地方,只要利用索引值( index ),就能在一瞬間找到元素。
大多數的演算法都運用了「索引」的技巧,讓程式執行速度更快。
以下介紹索引的幾種運用方式。是我自己歸類整理的,並不是標準。
一、定位
概念為:將物件放入陣列中, array[ 位置 ] = 物件。
當元素很多時,我們可以放到陣列裡。我們只要記錄索引值,依舊可以常數時間得到元素。
範例:求最大值。將元素連續地放入陣列,若想紀錄一元素,僅需一索引值。
求最大值
void find_maximum() // 用變數直接記最大值
{
int array[5] = {3, 6, 9, 8, 1};
int max = -10000;
for (int i=0; i<5; i++)
if (array[i] > max)
max = array[i];
cout << "最大值為" << max;
}
void find_maximum() // 用索引值紀錄最大值位置
{
int array[5] = {3, 6, 9, 8, 1};
int p = 0; // 最大值的索引值
for (int i=1; i<5; i++)
if (array[i] > array[p])
p = i;
cout << "最大值為" << array[p];
}
範例:求子字串。將元素連續地放入陣列,若想紀錄一區間,僅需頭尾的索引值。
求子字串
void substring()
{
char s[10] = "Hello, world!";
char t[10];
int i, j;
for (i=2, j=0; i<6; i++, j++)
t[j] = s[i];
t[j] = '\0';
cout << "s的子字串[2,6)是" << t;
}
範例:連續數字和。將元素連續地放入陣列,利用問題本身的數學性質以及索引值,快速得到答案。
連續數字和
void consecutive_sum()
{
int array[5] = {3, 6, 9, 8, 1};
int sum[5] = {0};
sum[0] = array[0];
for (int i=1; i<5; i++)
sum[i] = sum[i-1] + array[i];
cout << "區間[2,4]的連續數字和是" << sum[4] - sum[2-1];
}
範例:求中位數。將元素依照大小順序並連續地放入陣列,利用索引值得到位於中間的元素。
求中位數
void median()
{
int array[5] = {1, 3, 6, 8, 9}; // 由小到大排序
cout << "中位數是" << array[5/2];
}
範例:二分搜尋法( Binary Search )。將元素依照大小順序並連續地放入陣列,然後夾擠索引值。如果使用的是鏈接串列,因為元素們都沒有索引值,就無法使用二分搜尋法。
二分搜尋法(Binary Search)
void binary_search()
{
int array[5] = {1, 3, 6, 8, 9}; // 由小到大排序
int key = 3; // 搜尋陣列裡有沒有3
int left = 0, right = 5; // 索引值範圍
while (left < right)
{
int mid = (left + right) / 2;
if (array[mid] < pivot)
left = mid + 1;
else if (array[mid] > pivot)
right = mid - 1;
else if (array[mid] == pivot)
return mid;
}
return left;
}
範例:二元樹( Binary Tree )。元素的索引值對應到樹的結構,是一種特殊的定位。
二元樹(Binary Tree)
// 得到array[index]的左邊小孩的索引值
int left_child(int index) {return index * 2;}
// 得到array[index]的右邊小孩的索引值
int right_child(int index) {return index * 2 + 1;}
void binary_tree()
{
int array[5] = {3, 6, 9, 8, 1};
cout << "根為" << array[0];
cout << "根的左邊小孩是" << array[left(0)];
cout << "根的右邊小孩是" << array[right(0)];
}
範例:堆疊( stack )、佇列( queue )。元素連續地放入陣列,然後以改變索引值的方式,來動態增減堆疊及佇列的元素。
堆疊(stack)、佇列(queue)
void stack()
{
int stack[10]; // 一個堆疊
int top = 0; // 索引值
stack[top++] = 3; // push
stack[top++] = 6; // push
if (top > 0) // is empty?
int n = stack[--top]; // pop
}
void queue()
{
int stack[10]; // 一個佇列
int front = 0, rear = 0; // 索引值
queue[rear++] = 3; // push
queue[rear++] = 6; // push
if (front < rear) // is empty?
int n = queue[front++]; // pop
}
二、歸類並標記
概念為:物件直接作為陣列的索引值, array[ 物件 ] = 物件的屬性。
範例:正整數集合。物件是:正整數,物件的屬性是:是否在集合裡頭出現。
正整數集合
void add_element()
{
// 一個有限集合
bool set[100];
// 初始化為空集合
for (int i=0; i<100; i++) set[i] = false;
// 設定集合元素
set[5] = true; // 集合裡有5
set[3] = true; // 集合裡有3
set[3] = false; // 集合裡沒有3
}
範例:統計英文字母出現次數。物件是英文字母,物件的屬性是英文字母的出現次數。
統計英文字母出現次數
void count_letter()
{
char s[20] = "hi, I am a boy";
// 歸類並標記
// 26個字母,字母的ASCII值剛好連續
int count[26] = {0};
for (int i=0; s[i] != '\0'; i++)
if (s[i] >= 'a' && s[i] <= 'z')
count[ s[i] - 'a' ]++;
else if (s[i] >= 'A' && s[i] <= 'Z')
count[ s[i] - 'A' ]++;
// 印出英文字母的出現次數
for (int i=0; i<26; i++)
if (count[i] > 0)
cout << char('A' + i) << "的個數為" << count[i];
}
範例:計數排序法( Counting Sort )。索引值的大小順序,剛好也是元素的大小順序,故可用於排序。
計數排序法(Counting Sort)
void counting_sort()
{
// 假設陣列裡數值不重複
int array[5] = {3, 6, 9, 8, 1};
// 歸類並標記
bool count[100] = {false};
for (int i=0; i<5; i++)
count[ array[i] ] = true;
// 索引值的大小順序,剛好也是元素的大小順序。
for (int i=0, j=0; i<100 && j<5; i++)
if (count[i]) // 數值不重複
array[j++] = i;
}
範例:雜湊表( hash table )。元素的索引值由特殊方法決定,是一種特殊的歸類。
雜湊表(hash table)
int hash(int n) // 根據元素的數值來製造一個index
{
return n * 97 % 100;
}
void hash_table()
{
int array[5] = {3, 6, 9, 8, 1};
int table[100];
for (int i=0; i<5; i++)
{
// 替array[i]製造一個index
int index = hash(array[i]);
// 將array[i]放入hash table
table[index] = array[i];
}
}
三、轉換
概念為: array[ 物件 ] = 另一個物件。類似函數的概念。
範例:取代( Substitution )、移位( Transposition )。取代和移位是密碼學的基礎概念。取代是文字的轉換,移位則是位置的轉換。
取代(Substitution)、移位(Transposition)
void substitution()
{
char s[10] = "Hello!", t[10] = "";
// 建立轉換表格
char crypt[128];
for (int i=0; i<128; i++) crypt[i] = i;
crypt['!'] = 'w';
crypt['H'] = 'Y';
// 開始轉換
int n;
for (n=0; s[n] != '\0'; n++)
t[n] = crypt[ s[n] ];
t[n] = '\0';
}
void transposition()
{
char s[10] = "Hello!", t[10] = "";
// 建立轉換表格
int crypt[50];
for (int i=0; i<50; ++i) crypt[i] = i;
crypt[2] = 3;
crypt[3] = 5;
crypt[5] = 2;
// 開始轉換
int n;
for (n=0; s[n] != '\0'; n++)
t[n] = crypt[ s[n] ];
t[n] = '\0';
}
範例: page table 。作業系統的機制,可將虛擬位址轉換成實體位址,是位址的轉換。
page table
程式碼略。
附錄:定址的時間複雜度
當索引值大小為 N 時,有人認為定址的時間複雜度是 O(log2N) ,也有人認為是 O(1) 。這兩種說法都是有其依據的。
以數學的觀點來看: N 共有 log2N 個位元,用二元樹的概念,依照各個位元的數值是 0 或是 1 進行分支,分支到底後就完成定址了。所以定址的時間複雜度是 O(log2N) 。
以電路的觀點來看:一顆中央處理器可以平行處理 32 位元(現在已有 64 位元),只要是介於 0 到 2^32-1 的索引值,都可以在 1 單位時間完成定址,而不必用 32 單位時間來完成定址。所以定址的時間複雜度是 O(1) 。
討論演算法的時間複雜度時,我們傾向假設定址的時間複雜度是 O(1) 。
附錄:定址的範圍
方才提到一顆中央處理器可以平行處理 32 位元,理論上可以定址到 2^32 以內的位址。一個位址一般擁有 1byte 的記憶體大小,所以我們利用定址方式,可以運用的記憶體就有 2^32 * 1byte = 4GB 這麼多。
但是作業系統會保留一些位址、預留一些記憶體空間以維持系統運作,所以使用者實際可以運用的記憶體其實不到 4GB 。
當記憶體沒有插到 4GB 的時候,作業系統利用一種叫做 virtual memory 的技術,以硬碟空間補足記憶體不足 4GB 的部份。
位址是連續不斷的,我們寫程式也都直接假設位址對應到的記憶體空間是連續不斷的,然而實際上並不是連續的。作業系統運用一種叫做 paging 的技術,藉由 page table ,讓記憶體看起來是連續的。