前兩天同事遇到了一個小需求,想判斷一個集合是不是在另一個集合中存在,並且要求順序一致,然後一起討論了下應該怎麼做,有沒有什麼比較好的方式?下面分享一下我們想到的方法,如果你也有不同的想法也可以分享給我。
01解法一:序列化字串(不推薦)
這個方案的核心思想就是如果一個集合是另一個集合的連續子序列,那麼這兩個集合序列化成字串應該也還會有這樣的特性,即一個字串包含了另一個字串。我立馬想到了用string.Join把陣列拼接成字串,具體程式碼實現如下:
public static bool IsSubsequenceJoin(IEnumerable<string> main, IEnumerable<string> sub)
{
var mainString = string.Join(",", main);
var subString = string.Join(",", sub);
return mainString.Contains(subString);
}
其實聽到序列化我第一反應是感覺有坑的,因為序列化會涉及到一些特殊字元的問題,還記得我之前的文章《“hello”.IndexOf(“\0”,2)中的坑》嗎,就是因為特殊字串引起。
當然上面的方法應對大多數需求應該沒問題,但是還是有些特例需要注意,比如下面這個例子:
string[] main = ["a", "b", "c", "d,e"];
string[] sub = ["d", "e"];
var isSubsequenceJoin = ContinuousSubsequence.IsSubsequenceJoin(main, sub);
Console.WriteLine("陣列 [\"a\", \"b\", \"c\", \"d,e\"] 序列化後: " + string.Join(",", main));
Console.WriteLine("陣列 [\"d\", \"e\"] 序列化後: " + string.Join(",", sub));
Console.WriteLine("mainString.Contains(subString) 結果: " + isSubsequenceJoin);
執行結果如下:
因為string.Join使用了“,“作為拼接,所以sub陣列序列化後就是"d,e",而陣列main中恰好有個元素"d,e",這就導致預期之外的錯誤結果。
既然string.Join方法有缺陷還有其他序列化方法嗎?我們試試專門做序列化的方法JsonConvert.SerializeObject,程式碼如下:
public static bool IsSubsequenceSerialize(IEnumerable<string> main, IEnumerable<string> sub)
{
var mainString = JsonConvert.SerializeObject(main).TrimStart('[').TrimEnd(']');
var subString = JsonConvert.SerializeObject(sub).TrimStart('[').TrimEnd(']');
return mainString.Contains(subString);
}
這個方法需要我們處理一些序列化產生的額外結構資料,比如上面的TrimStart('[').TrimEnd(']'),就是為了去掉陣列產生的額外字元,如果要是其他更復雜型別可以要考慮的情況更多。
當然這個方法就可以解決上面的特例問題,但是依然有些特殊情況會導致錯誤,比如下面的示例:
string[] main = ["a", "b", "c", "d,\"e"];
string[] sub = ["e"];
var isSubsequenceSerialize = ContinuousSubsequence.IsSubsequenceSerialize(main, sub);
Console.WriteLine("IsSubsequenceSerialize 方法: ");
Console.WriteLine("陣列 main [\"a\", \"b\", \"c\", \"d,\\\"e\"] 序列化後: " + JsonConvert.SerializeObject(main).TrimStart('[').TrimEnd(']'));
Console.WriteLine("陣列 sub [\"e\"] 序列化後: " + JsonConvert.SerializeObject(sub).TrimStart('[').TrimEnd(']'));
Console.WriteLine("mainString.Contains(subString) 結果: " + isSubsequenceSerialize);
結果如下:
這個例子因為我們在構建陣列main時,在一個元素中多加了一個符合[ “ ],使得結果出現預期之外的錯誤結果。
因此對於序列化字串方式在大多數情況下都是不推薦使用的,除非你能保證你的資料不會出現一些特殊情況,如果數量大可能還會有效能問題。
02解法二:滑動視窗(推薦)
我們簡單解釋一下滑動視窗演算法是啥意思,先看一下下面這張圖感受一下。
滑動視窗可以理解成在一個長軸上,滑動一個視窗,因為視窗大小是固定的,所以隨著時間推移,從視窗中看到的東西是不一樣的。
把我們的問題和上圖結合起來看,如果主陣列main代表長軸,子陣列sub代表視窗,現在我們把子陣列視窗對齊主陣列起始位置,然後時間軸每前進一個刻度代表子陣列沿著主陣列向前移動一位數。
當時間刻度來到T2時,從子陣列視窗中可以看到陣列[1,3,5],當時間刻度來到T8時,從子陣列視窗中可以看到陣列[8,7,6]。
也就是說隨著時間的推進,當子陣列視窗中看到的陣列和自身陣列值一樣時,則代表子陣列是主陣列的連續子序列。具體實現程式碼如下:
public static bool IsSubsequenceSlidingWindow<T>(IEnumerable<T> main, IEnumerable<T> sub)
{
var mainLength = main.Count();
var subLength = sub.Count();
for (int i = 0; i < mainLength - subLength + 1; i++)
{
var expect = main.Skip(i).Take(subLength);
if (expect.SequenceEqual(sub))
{
return true;
}
}
return false;
}
注: 相關原始碼都已經上傳至程式碼庫,有興趣的可以看看。
https://gitee.com/hugogoos/Planner