我不熟悉的list
其實在日常中,連結串列的題目做的比較多,但是使用STL自帶連結串列的還是比較少,所以裡面的一些API不大熟悉。這邊也簡要介紹一些。
基本的一些API
先列舉的這些和上面幾篇用法幾乎一樣,所以不再累述。
賦值相關
list(beg,end);//建構函式將[beg, end)區間中的元素拷貝給本身。 list(n,elem);//建構函式將n個elem拷貝給本身。 list(const list &lst);//拷貝建構函式。 assign(beg, end);//將[beg, end)區間中的資料拷貝賦值給本身。 assign(n, elem);//將n個elem拷貝賦值給本身。 list& operator=(const list &lst);//過載等號操作符 swap(lst);//將lst與本身的元素互換
插入、刪除相關
push_back(elem);//在容器尾部加入一個元素 pop_back();//刪除容器中最後一個元素 push_front(elem);//在容器開頭插入一個元素 pop_front();//從容器開頭移除第一個元素 insert(pos,elem);//在pos位置插elem元素的拷貝,返回新資料的位置。 insert(pos,n,elem);//在pos位置插入n個elem資料,無返回值。 insert(pos,beg,end);//在pos位置插入[beg,end)區間的資料,無返回值。 clear();//移除容器的所有資料 erase(beg,end);//刪除[beg,end)區間的資料,返回下一個資料的位置。 erase(pos);//刪除pos位置的資料,返回下一個資料的位置。 remove(elem);//刪除容器中所有與elem值匹配的元素。
list提供了可以從頭部或尾部直接加入元素,也可以在頭部或尾部直接刪除元素。
把remove函式拿出來講一下,如何刪除自定義資料。如果我們的資料是內建型別,那很好刪除,找到該elem,用 == 比較,為真就表示找到了,進行刪除。而自定義資料用 == 是不能直接比較的,我們應該先過載==,再進行刪除。 我們過載的時候,要注意,傳入的引數都必須是const型別,否則編譯不會通過,因為編譯器發覺我們會有改變資料的風險,導致比較結果出錯!
大小操作
size();//返回容器中元素的個數 empty();//判斷容器是否為空 resize(num);//重新指定容器的長度為num, 若容器變長,則以預設值填充新位置。 如果容器變短,則末尾超出容器長度的元素被刪除。 resize(num, elem);//重新指定容器的長度為num, 若容器變長,則以elem值填充新位置。 如果容器變短,則末尾超出容器長度的元素被刪除。
這邊的size函式有個特殊點:
- GCC下時間複雜度為O(n).
- VC下時間複雜度為O(1).
在GCC下,size函式的複雜度為O(n),它是通過呼叫標準庫的distance(begin(),end())演算法來計算長度的。而在VC類的編譯器下,維護了一個成員變數_Mysize,儲存長度。就可以直接查出來,時間複雜度為O(1).
這其實是一種取捨,要先講一下list::splice函式,這是讓兩個不同的連結串列進行拼接的函式。GCC為了維護這個函式,使拼接後長度更容易計算,因此沒有給一個成員變數來儲存大小,如果給了,那拼接後的長度是兩個size相加嗎?不一定,因為splice拼接方法有多種。所以沒有給出。
存取操作
front();//返回第一個元素。 back();//返回最後一個元素。
注意是直接返回元素。
STL中list是一個雙向迴圈連結串列
我們用一段程式碼來證明它是一個雙向迴圈連結串列:從頭結點遍歷2倍的長度單位,看是否會迴圈再列印一次。
先說一下幾個型別:
//連結串列結點型別 list<int>::_Nodeptr node;
而結點類有三個成員:下一節點指標,上一節點指標,資料:
node->_Next; node->_Prev; node->_Myval;
再看例子:
int main(){ list<int> myList; for (int i = 0; i < 10; i ++){ myList.push_back(i); } list<int>::_Nodeptr node =myList._Myhead->_Next; for (int i = 0; i < myList._Mysize * 2;i++){ cout << "Node:" << node->_Myval << endl; node = node->_Next; if (node == myList._Myhead){ node = node->_Next; } } return 0; }
首先我們用一個for迴圈,將資料存進去。
然後我們用一個節點指向頭結點的的下一節點。 list的頭結點_Myhead是不存任何東西的,只是為了插入方便而已,所以這麼整。所以我們要指向第一個有資料的節點。
當我們遍歷一次連結串列長度時,到達尾節點時,若是迴圈連結串列,則會再回到頭部,看一下執行結果:
可以清楚的看到,我迴圈了2倍長度,它遍歷了兩次。所以,list是一個迴圈連結串列。
list的反轉和排序函式
reverse和sort函式
reverse函式就是反轉函式,較為簡單,沒什麼特殊的。sort函式排序也是一樣,預設從小到大排序。
而我們可以是指定排序順序的。有兩種方法,一種是回撥函式的方法,另一種是仿函式的方法。
現在假設我們有一個連結串列:
list<int> myList; for (int i = 0; i < 5; i++){ myList.push_back(i); } list.sort();
我們執行完sort的結果,會是從小到大排序的連結串列。也就是說,當sort的引數為空時,會執行預設的排序, 那現在我們就給它傳遞一個引數,改變排序規則。
現在就來介紹上述的兩種方法。
回撥函式方法
先寫出這個回撥函式:
bool mycmp(int a, int b) { return a > b; }
- 返回值:bool。
- 引數:int型(取決於你的連結串列存放的資料型別)。
- 排序規則:從大到小,所以return a > b;
我們針對整型進行大到小的排序,所以,我們要返回a > b即可。
隨後,我們再程式中執行:
mylist.sort(mycmp);
這樣就行了,就可以從大到小排序了。
仿函式方法
記住仿函式不是一個函式,是一個類。它通過過載()來生效。因為底層中使用了大量的cmp(a,b)這種形式來比較a和b的大小。來看一下:
class MyCmp { public: bool operator()(int a, int b) { return a > b; } };
同樣的,仿函式裡面過載():
- 返回值:bool。
- 引數:int型(取決於你的連結串列存放的資料型別)。
- 排序規則:從大到小,所以return a > b;
可以看到,和回撥函式的規則幾乎一樣。
呼叫形式為:
mylist.sort(MyCmp());
為什麼多了一對小括號呢?我們需要傳入的是一個物件,而回調函式傳進去就直接是一個函式物件,仿函式是一個類,我們加一對括號生成一個匿名物件,從而傳遞正確的引數。
下面看一個reverse和sort的使用例子:
int main() { list<int> myList; for (int i = 0; i < 5; i++){ myList.push_back(i); } //反轉 myList.reverse(); list<int>::_Nodeptr node = myList._Myhead->_Next; cout << "反轉之後:" << endl; for (size_t i = 0; i < myList._Mysize; ++i) { cout << node->_Myval << " "; node = node->_Next; } //再排序(從小到大) myList.sort(); node = myList._Myhead->_Next; cout << endl << "預設排序:" << endl; for (size_t i = 0; i < myList._Mysize; ++i) { cout << node->_Myval << " "; node = node->_Next; } //再排序(從大到小) //myList.sort(mycmp);//使用回撥函式方法,傳入一個函式物件 myList.sort(MyCmp());//使用仿函式方法,傳入一個匿名物件 node = myList._Myhead->_Next; cout << endl << "從大到小排序:" << endl; for (size_t i = 0; i < myList._Mysize; ++i) { cout << node->_Myval << " "; node = node->_Next; } return 0; }
執行結果:
為什麼不用標準庫演算法sort呢?
如果用系統提供的sort,那麼引數就為迭代器:
sort(mylist.begin(),mylist.end());
但實際上不能完成的。因為系統提供的sort函式對迭代器有要求: 必須是可以隨機訪問的迭代器! 而list容器不提供隨機訪問的能力,所以不能使用。但是往往這類東西都會自己實現一個sort,所以不用擔心。
一些複雜需求排序
講完剛剛指定排序規則的排序,現在我們可以有更復雜的排序需求了:
若干學生,優先按照成績大到小排序,成績相同按照年齡小到大排序,年齡相同按照姓名升序排序,姓名相同按照班級降序排序。
這就是個不斷比較的過程而已,沒什麼特殊的地方,回撥函式如下:
bool ComplicatedCmp(Stu &stu1, Stu &stu2) { if (stu1._iScore == stu2._iScore) { if (stu1._iAge == stu2._iAge) { if (stu1._strName == stu2._strName) { return stu1._iClass > stu2._iClass; } else { return stu1._strName < stu2._strName; } } else { return stu1._iAge < stu2._iAge; } } else { return stu1._iScore > stu2._iScore; } }
主函式:
int main() { list<Stu> mylist; mylist.push_back(Stu("Tom", 4, 90, 13)); mylist.push_back(Stu("Jerry", 5, 84, 11)); mylist.push_back(Stu("Giao", 1, 99, 10)); mylist.push_back(Stu("Fuck", 6, 35, 15)); mylist.push_back(Stu("Bill", 2, 96, 17)); mylist.push_back(Stu("Null", 2, 96, 16)); mylist.push_back(Stu("Null", 0, 96, 17)); mylist.sort(ComplicatedCmp); for (auto it = mylist.begin(); it != mylist.end(); ++it) { cout << "Score:" << it->_iScore << "Age:" << it->_iAge << "Name:" << it->_strName << "Class:" << it->_iClass << endl; } return 0; }
可以看到結果為:
已經按照我們想要的排序進行排序了。