前言
順序表其實就是數組,而字符串的區間操作,其實就是對一個字符串裏的一段進行操作,例如刪除一段字符、插入一段字符、獲取一段字符等。這原本是我的一次數據結構課程作業,來自李春葆的數據結構教程第五版第四章的上機實驗題,雖不難但細節較多,算是一次不錯的訓練編碼能力的機會。
數據定義
既然要實現字符串的區間操作,那麼肯定需要先手擼一個簡易的字符串,先實現一些常規的方法,例如創建字符串、銷燬字符串、判空等,由於書本上是用的c語法來實現的,也就是結構體+全局函數的形式,沒有包裝成C++類的形式,索性我也按照這種方式實現。
先定義一個結構體,按照數據結構教材裏慣用的結構體定義方式,由於這次要寫的是SequenceString教材裏簡稱爲SqString,這個結構體我就簡稱爲SS吧,後面寫起來也方便點:
#define MAXSIZE 20
typedef struct SqList
{
char *arr;// 字符數組
int size;// 代表整個字符串的長度,同時也代表着數組的第一個空位置的下標
} SS; // Sequence String
基礎函數的實現
一些常規、必須的方法的實現:
SS *createSS()// 創建空字符串
{
SS *ss = new SS;
ss->arr = new char[MAXSIZE];
ss->size = 0;//沒存任何字符的時候爲0,代表長度爲0,同時代表第一個空位置下標爲0
memset(ss->arr, '0', MAXSIZE * sizeof(char));
return ss;
}
SS *createSS(const char *str)//根據一個已有的c風格字符串來創建我們的字符串
{
SS *ss = new SS;
ss->arr = new char[MAXSIZE];
memset(ss->arr, '0', MAXSIZE * sizeof(char));
strncpy(ss->arr, str, strlen(str));
ss->size = strlen(str);
ss->arr[ss->size] = '\0';
return ss;
}
void destroySS(SS *ss)//銷燬字符串
{
if (!ss)
return;
delete[] ss->arr;// new [] 對 delete []
delete ss; // new 對 delete
}
bool isEmpty(SS *ss)//判空
{
return !ss->size;
}
bool isFull(SS *ss)//判滿
{
return ss->size == MAXSIZE;
}
int getSize(SS *ss)//取長度
{
return ss->size;
}
void print(SS *ss)//打印字符串
{
if (!ss)
{
cout << "print(NULL)." << endl;
return;
}
for (int i = 0; i < ss->size; i++)
cout << ss->arr[i];
cout << endl;
}
SS *copy(SS *ori)// 返回一個深拷貝的ori,其實就是把ori完整複製一遍
{
SS *ss = createSS();
for (int i = 0; i < ori->size; i++)
ss->arr[i] = ori->arr[i];
ss->size = ori->size;
return ss;
}
以上是一些基本的函數實現,在create函數中我爲了調試方便,直接將數組的空間格式化成了字符'0',並且重載了一個根據已有c風格string構建我們的SqString的方法。這些都不是本篇的重點,簡單略過就行,如果有不太清楚的可以看看代碼中的註釋。
區間插入
其實就是在字符串中插入一個子串,思路很簡單,先給要插入的子串挪出空位,然後插入,當然,要先經過繁瑣的下標合法檢查。函數定義:
SS *insert_range(SS *ori, int startIndex, const char *str, bool returnNewSS)
這個ori就是即將被插的SqString,要插入的是後面的str,startIndex是插入位置,從1開始,而這個returnNewSS代表是否要返回一個新的對象,如true,ori不會被改動,返回新的修改好的對象,如false,則返回修改後的ori。
代碼實現:
SS *insert_range(SS *ori, int startIndex, const char *str, bool returnNewSS)
{
int length = strlen(str);//因爲是c風格字符串,所以可以先用strlen計算出長度
if (ori->size + length >= MAXSIZE)//結果長度大於數組的最大長度
{
cout << "The length of result SqString is longer than the MaxSize." << endl;
return NULL;
}
if (startIndex < 1 || startIndex > ori->size + 1)//插入位置爲負或者超過字符串長度
{
cout << "Index illegal." << endl;
return NULL;
}
SS *ss = (returnNewSS ? copy(ori) : ori);//根據參數決定是否複製
//下面就是挪出空間給串str的代碼,從尾部開始到startIndex指向的下標爲止向後挪length位
for (int i = ss->size - 1; i >= startIndex - 1; i--)
{
ss->arr[i + length] = ss->arr[i];
}
ss->size += length;//別忘了維護size
//將str賦到ss中空出的位置去
int j = 0;
for (int i = 0; i < length; i++, j++)
{
ss->arr[startIndex - 1 + i] = str[j];
}
return ss;
}
思路是:先將 從startIndex開始到數組尾部 的這些字符往後移length位,騰出length個位置,這個length就是要插入的子串str的長度;然後再把str賦到這length個空位上就可以了。
代碼中有很詳細的註釋,另外要注意的是:
1.startIndex是從1開始的,轉成數組下標時需要-1
2.size作爲下標的話指向的是數組尾部第一個爲空的字符,如果要指向最後一個有效字符需要-1,例如size爲3的數組,最後一個有效字符的下標就是2,而下標3就是尾部第一個爲空的字符
3.這個插入位置的含義需要多注意一下,例如對於串"abc",我們在第一個位置插入子串"111"的話,得到的是"111abc",第一個位置的字符被擠到後面去了
4.我的這個實現是支持在尾部插入的,例如對於長度爲3的串"abc",調用函數在第4個位置插入子串"111"的話是可以得到串"abc111"的, 而如果在第5個位置插入就會報錯
要是還搞不懂的話,建議拿個草稿紙畫畫,容易理清思路。
區間刪除
函數定義:
SS *del_range(SS *ori, int startIndex, int delLength, bool returnNewSS)
參數的定義同上,只不過這次是從串ori中刪除一段字符,只需要開始位置和要刪除的子串的長度就可以了。
代碼實現:
SS *del_range(SS *ori, int startIndex, int delLength, bool returnNewSS)
{
int endIndex = startIndex - 1 + delLength - 1;//這裏計算要刪除的區間的最後一個字符的下標
if (startIndex < 1 || startIndex > ori->size)
{
cout << "startIndex illegal." << endl;
return NULL;
}
if (endIndex > ori->size -1)//endIndex是下標,size也應該轉成下標來比較
{
cout << "endIndex illegal." << endl;
return NULL;
}
SS *ss = (returnNewSS ? copy(ori) : ori);
//下面是區間刪除的具體實現
for (int i = 0; i < ss->size - 1 - endIndex; i++)
ss->arr[i + startIndex - 1] = ss->arr[i + startIndex - 1 + delLength];
ss->size -= delLength;
return ss;
}
函數開始和上面區間插入的實現差不多,先判斷參數是否合法,然後根據是否返回新對象拷貝對象,不同的是endIndex的計算和最後刪除區間的具體實現,待我慢慢解釋。
首先,startIndex是待刪除區間的起始位置,是從1開始的,且是包含在刪除區間裏的,length是待刪除區間的長度,那麼要計算待刪除區間的結束下標(結束下標也是包含在結束區間裏的,整個區間爲閉區間),就應該是 起始下標 + 區間長度 - 1,而起始下標又等於startIndex-1,所以最後 endIndex = startIndex -1 + length -1,第一個 -1 來自startIndex和下標的差值,第二個-1是因爲startIndex所指下標是包含在待刪除區間裏的。
剛好手頭有畫板,以在第2個位置開始刪除3個字符爲例:
獻醜了,哈哈..
endIndex的計算解釋完了,然後就是解釋最後的那個for了:
for (int i = 0; i < ss->size - 1 - endIndex; i++)
ss->arr[i + startIndex - 1] = ss->arr[i + startIndex - 1 + delLength];
其實這裏的思路是把待刪除區間後面的字符往前挪,覆蓋掉待刪除區間的字符就可以了,然後維護一下ss的size就行。
這段代碼做的就是 先計算出實際要挪的字符數量 ss->size - 1 - endIndex(其實就是從endIndex到數組尾的長度),然後從0開始for,for裏面用偏移(就是待刪除區間的長度)把待刪除的字符用待刪除區間後面的字符覆蓋。
這段代碼的大致思路就是這樣,如果還不理解的話同樣推薦畫圖理一下思路。
區間提取
其實就是根據開始位置和長度來截取一段字符作爲子串,函數實現起來要比上面兩個操作簡單不少,直接上代碼:
SS *get_range(SS *ori, int startIndex, int length)
{
int endIndex = startIndex - 1 + length - 1;
if (startIndex < 1 || startIndex > ori->size)
{
cout << "startIndex illegal." << endl;
return NULL;
}
if (endIndex > ori->size - 1 || length < 0)
{
cout << "endIndex illegal." << endl;
return NULL;
}
SS *ss = createSS();
for (int i = 0; i < length; i++)
{
ss->arr[i] = ori->arr[startIndex - 1 + i];
}
ss->size = length;
return ss;
}
就是根據長度將目的區間的字符賦值到新串裏就是了,沒什麼難點,至於區間的set啥的也跟這個很類似,就不講了。
完整代碼
還包含了一些作業要求的函數,這裏就不再解釋了。
#include <iostream>
#include <cstring>
#define MAXSIZE 20
using namespace std;
typedef struct SqList
{
char *arr;
int size;
} SS;
SS *createSS()
{
SS *ss = new SS;
ss->arr = new char[MAXSIZE];
ss->size = 0;
memset(ss->arr, '0', MAXSIZE * sizeof(char));
return ss;
}
SS *createSS(const char *str)
{
SS *ss = new SS;
ss->arr = new char[MAXSIZE];
memset(ss->arr, '0', MAXSIZE * sizeof(char));
strncpy(ss->arr, str, strlen(str));
ss->size = strlen(str);
ss->arr[ss->size] = '\0';
return ss;
}
void destroySS(SS *ss)
{
if (!ss)
return;
delete[] ss->arr;
delete ss;
}
bool isEmpty(SS *ss)
{
return !ss->size;
}
bool isFull(SS *ss)
{
return ss->size == MAXSIZE;
}
int getSize(SS *ss)
{
return ss->size;
}
void print(SS *ss)
{
if (!ss)
{
cout << "print(NULL)." << endl;
return;
}
for (int i = 0; i < ss->size; i++)
cout << ss->arr[i];
cout << endl;
}
SS *copy(SS *ori)
{
SS *ss = createSS();
for (int i = 0; i < ori->size; i++)
ss->arr[i] = ori->arr[i];
ss->size = ori->size;
return ss;
}
void add(SS *ss, int index, char data)
{
if (isFull(ss))
{
cout << "SqString is full." << endl;
return;
}
if (index < 1 || index > ss->size + 1)
{
cout << "index illegal." << endl;
return;
}
for (int i = ss->size; i > index - 1; i++)
{
ss->arr[i] = ss->arr[i - 1];
}
ss->arr[index - 1] = data;
ss->size++;
}
int del(SS *ss, int index)
{
if (isEmpty(ss))
{
cout << "SqString is empty." << endl;
return INT_MIN;
}
if (index < 1 || index > ss->size)
{
cout << "index illegal." << endl;
return INT_MIN;
}
int res = ss->arr[index - 1];
for (int i = index - 1; i < ss->size - 1; i++)
{
ss->arr[i] = ss->arr[i + 1];
}
ss->size--;
return res;
}
void set(SS *ss, int index, char data)
{
if (isEmpty(ss))
{
cout << "SqString is empty." << endl;
return;
}
if (index < 1 || index > ss->size)
{
cout << "index illegal." << endl;
return;
}
ss->arr[index - 1] = data;
}
SS *insert_range(SS *ori, int startIndex, const char *str, bool returnNewSS)
{
int length = strlen(str);
if (ori->size + length >= MAXSIZE)
{
cout << "The length of result SqString is longer than the MaxSize." << endl;
return NULL;
}
if (startIndex < 1 || startIndex > ori->size + 1)
{
cout << "Index illegal." << endl;
return NULL;
}
SS *ss = (returnNewSS ? copy(ori) : ori);
for (int i = ss->size - 1; i >= startIndex - 1; i--)
{
ss->arr[i + length] = ss->arr[i];
}
ss->size += length;
int j = 0;
for (int i = 0; i < length; i++, j++)
{
ss->arr[startIndex - 1 + i] = str[j];
}
return ss;
}
SS *del_range(SS *ori, int startIndex, int delLength, bool returnNewSS)
{
int endIndex = startIndex - 1 + delLength - 1;
if (startIndex < 1 || startIndex > ori->size)
{
cout << "startIndex illegal." << endl;
return NULL;
}
if (endIndex > ori->size - 1)
{
cout << "endIndex illegal." << endl;
return NULL;
}
SS *ss = (returnNewSS ? copy(ori) : ori);
for (int i = 0; i < ss->size - 1 - endIndex; i++)
ss->arr[i + startIndex - 1] = ss->arr[i + startIndex - 1 + delLength];
ss->size -= delLength;
return ss;
}
SS *get_range(SS *ori, int startIndex, int length)
{
int endIndex = startIndex - 1 + length - 1;
if (startIndex < 1 || startIndex > ori->size)
{
cout << "startIndex illegal." << endl;
return NULL;
}
if (endIndex > ori->size - 1 || length < 0)
{
cout << "endIndex illegal." << endl;
return NULL;
}
SS *ss = createSS();
for (int i = 0; i < length; i++)
{
ss->arr[i] = ori->arr[startIndex - 1 + i];
}
ss->size = length;
return ss;
}
SS *set_range(SS *ori, int startIndex, const char *str, bool returnNewSS)
{
int length = strlen(str);
int endIndex = startIndex - 1 + length;
if (startIndex < 1 || startIndex > ori->size)
{
cout << "startIndex illegal." << endl;
return NULL;
}
if (endIndex > ori->size - 1)
{
cout << "The length of str is illegal." << endl;
return NULL;
}
SS *ss = (returnNewSS ? copy(ori) : ori);
for (int i = 0; i < length; i++)
{
ss->arr[i + startIndex - 1] = str[i];
}
return ss;
}
SS *append(SS *s1, SS *s2, bool returnNewSS) // Link s1 and the s2
{
if (s1->size + s2->size > MAXSIZE)
{
cout << "The length of result string is longer than MaxSize." << endl;
return NULL;
}
SS *ss = (returnNewSS ? copy(s1) : s1);
for (int i = 0; i < s2->size; i++)
{
ss->arr[i + ss->size] = s2->arr[i];
}
ss->size += s2->size;
return ss;
}
SS *replace(SS *ori, int startIndex, int length, const char *str, bool returnNewSS)
{
if (length < strlen(str))
{
cout << "replacing length is shorter than the length of str." << endl;
return NULL;
}
SS *ss = del_range(ori, startIndex, length, true);
insert_range(ss, startIndex, str, false);
return ss;
}
int main()
{
SS *ss = createSS("abcdef");
print(ss);
SS *s1 = get_range(ss, 2, -1);
print(s1);
destroySS(ss);
destroySS(s1);
return 0;
}
總結
數組的區間操作其實沒什麼很難的內容,無非就是兩個要注意的地方:
一是起始位置是從0開始的還是從1開始的問題
要解決這個問題就是要理清這裏面的關係,如果傳進來的索引是從0開始的,那就直接當下標用,而且在計算區間長度、計算區間端點的時候也要與相應的下標相減才行,不然一個是從1開始的索引,一個是從0開始的下標,計算得到的長度就是錯的;如果是從1開始的,那麼轉換成下標的時候就要記得-1。
二是區間端點問題
本文中的區間全都是閉區間,對於起始位置爲startIndex,長度爲length的區間來說,範圍就是
[startIndex ,startIndex + length],要是以數組下標表示的話,則是[startIndex -1 ,startIndex -1 + length] 。但是由於區間的起始位置是包含在區間裏面的,我們計算終點座標的時候應該是 終點座標 = startIndex -1 + length -1 纔對;
舉個例子,對於串“abcde”,長度爲5,問起始位置(位置,不是數組下標,從1開始)爲2,長度爲3的區間終點的位置?位置爲2的字符是‘b’,從‘b’開始數三個字符,是“bcd”,那麼最後一個區間最後一個字符就是‘d’,‘d’的位置是4,順着數第四個字符嘛,所以要是直接2 + 3 = 5就得到區間終點的位置是不對的,還需要-1才行。
區間操作的代碼實現起來其實沒什麼難的,主要是因爲我自己以前學C的時候學得不夠紮實,練得少,導致這種處理下標、區間端點的問題總是感覺很苦手,正好借這次數據結構作業的機會練習這部分的內容,同時也整理成博文跟大家分享,那麼本篇文章就到這裏結束,感謝閱讀。