鏈表
鏈表是一個線性順序結構,鏈表爲動態集合提供了一種簡單而靈活的表示方法,但由於指針的存在,在代碼的書寫過程中很容易出bug。不管是利用其他技巧模擬指針還是語言自帶的指針都很容易出錯。
下面給出鏈表的基本操作代碼:の鏈表的結構圖示就不給出了,網上搜索一大吧。
struct linkList{
node * head;
linkList(){
head = nullptr;
}
node * search(int val){
node * x = head;
while(x != nullptr && x->val != val){ //如果是循環鏈表還要判斷是否已經循環了
x = x->next;
}
return x;
}
void insert(int val){
node * x = new node();
x->val = val;
if(head != nullptr)
head->prev = x;
x->next = head;
x->prev = nullptr;
head = x;
}
void listDelete(int v){
node * x = search(v);
listDelete(x);
}
void listDelete(node * x){
if(x->prev != nullptr)
x->prev->next = x->next;
else head = x->next;
if(x->next != nullptr)
x->next->prev = x->prev;
}
void out(){
node * x = head;
while(x !=nullptr){
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
就在寫上面的示例代碼的時候就出錯了一次,還是簡單的示例代碼。並沒有什麼複雜的操作。便會有隱蔽的錯誤。
哨兵
哨兵是一個啞對象,其作用是簡化邊界條件的處理。如在循環雙向鏈表中設置一個哨兵節點。且哨兵節點的prev和next互相指着。這樣一個空的循環雙向鏈表便存在了。對其的操作只需要將中間變量指針temp指向哨兵節點的next接下來只要判斷temp是否指向哨兵節點便可判斷是否遍歷完該鏈表。
上面的鏈表實現並沒有使用哨兵,使用哨兵節點可以簡化代碼。 但也僅限於簡化代碼。不能起到減小時間複雜度的作用(雖然在刪除和插入的時候節省了判斷head是否爲空的操作);而且當需要大量的小鏈表來存儲數據的時候,哨兵節點浪費的大量內存。下面給出使用哨兵的雙向循環鏈表的代碼:
struct linkListPlus{
node * head;
linkListPlus(){
head = new node();
head->prev = head;
head->next = head;
}
node * search(int val){
node * x = head->next;
while(x != head && x->val != val){
x = x->next;
}
return x;
}
void insert(int val){
node * x = new node();
x->val = val;
x->next = head->next;
head->next->prev = x;
head->next = x;
x->prev = head;
}
void listDelete(int v){
node * x = search(v);
listDelete(x);
}
void listDelete(node * x){
cout << x->val << endl;
x->prev->next = x->next;
x->next->prev = x->prev;
}
void out(){
node * x = head->next;
while(x !=head){
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
習題:
1. 單鏈表上的動態集合操作insert能否在O(1)時間內實現?delete操作呢?
答:不能,刪除一個元素需要便利集合(順序結構)找到該元素,然後改變前後指針的值達到刪除的目的。將元素直接添加在鏈表頭部確實可以做到O(1) 複雜度,但是一個集合是不允許擁有重複元素,這時需要遍歷集合查重。最終兩個操作在最壞情況下的時間複雜度爲O(N);
2. 用一個單鏈表L實現一個棧。要求操作push和pop的運行時間仍爲O(1).
答:代碼如下:
struct listStack{
node * head;
listStack(){
head = new node();
head->prev = head->next = head;
}
int isEmpty(){
return head->next == head;
}
void push(int v){
node * x = new node();
x->next = head->next;
x->val = v;
head->next = x;
}
int pop(){
int x;
if(isEmpty())
return -1;// 發生溢出, 沒有異常處理 所以返回-1代表異常
node * temp = head->next;
head->next = temp->next;
x = temp->val;
delete temp; //執行空間回收操作。避免內存泄漏
return x;
}
void out(){
printf("data: ");
node * x = head->next;
while(x !=head){
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
3. 用一個單鏈表L實現一個隊列,要求操作enqueue和dequeue的運行時間仍爲O(1).
答:代碼如下:
struct listQueue{
node * front; // front->next 代表當前隊首元素, front->prev代表隊尾元素
listQueue(){
front = new node();
front->next = front->prev = front;
front->val = -1; // 哨兵節點front的值val可以做調用,調試的時候可給其他節點賦值爲非-1的值 這樣輸出-1代表該執政指向哨兵節點
}
bool isEmpty(){
return front->next == front;
}
void enqueue(int v){
node * temp = new node();
temp->val = v;
temp->next = front->prev->next; //糊塗的用了錯誤代碼:temp->next = front->prev;
front->prev->next = temp;
front->prev = temp;
}
int dequeue(){
if(isEmpty())
return -1;//溢出
int x;
node * temp = front->next;
front->next = temp->next;
x = temp->val;
delete temp;
return x;
}
void out(){
node * x = front->next;
while(x != front)
{
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
4. 在O(N)時間內反轉一個鏈表,不能使用額外空間。
答:代碼如下:交換每個節點prev和next指針的值即可。
void reverse(){
puts("reverse");
node * x = head, *next=head->next;
while(next != head){
next = x->next;
swap(x->next, x->prev);
x = next;
}
}
5. 實現只有一個指針的雙向循環鏈表,題目提示使用整數值來表示指針,0表示空指針。這樣即便有32爲的整數,指針的範圍也限制在一定範圍內。先可一試:
struct onePointNode {
size_t p; //前16爲代表prev, 後16位代表next 注意 前十六位代表高位十六位
int val;
const static int prev = 4294901760;
const static int next = 65535;
onePointNode() {
p = val = 0;
}
void setPrev(int x) {
p &= next; // 先將原來的prev部分
p |= (x<<16);
}
int getPrev() {
return (p >> 16) & next;
}
void setNext(int x) {
p &= prev; //清空舊的next部分
p |= x;
}
int getNext() {
return p & next;
}
};
struct onePointLinkList {
const static int N = 1000;
onePointNode dat[N];
stack<int> free; //充分利用空餘空間 簡單模擬的垃圾回收 重複利用機制
int head;
onePointLinkList() {
for(int i = N-1; i > 0; i--) {
free.push(i);
}
head = 0;
}
void insert( int v) {
int x = free.top();
free.pop();
dat[x].val = v;
dat[dat[head].getNext()].setPrev(x); //循環鏈表中哨兵節點的前驅指向空鏈表條件下插入的第一個元素
dat[x].setNext(dat[head].getNext());
dat[x].setPrev(head);
dat[head].setNext(x);
}
int search(int v) {
int x = dat[head].getNext();
while(x != 0 && dat[x].val != v) {
x = dat[x].getNext();
}
return x;
}
void nodeDelete(int v) {
int x = search(v);
if(x != 0) {
dat[dat[x].getNext()].setPrev(dat[x].getPrev());
dat[dat[x].getPrev()].setNext(dat[x].getNext());
free.push(x);
}
}
void out() {
int x = dat[head].getNext();
while(x != 0) {
printf("%d ", dat[x].val);
x = dat[x].getNext();
}
puts("");
}
};
在不支持顯式指針數據類型的編程環境下實現鏈表
在不支持顯式指針的環境下可以利用上面的int類型的位運算+數組的方式實現簡單的但元素個數控制在一定範圍內的鏈表。還可以用但數組模擬同構對象的鏈表和使用多數組而不是上面的結構體數組實現。不過都是利用其他技巧模擬指針達到目的。所以這裏就不另加編程實現了。以後有機會補上。緊湊的模擬鏈表
上面給出的利用位運算實現的鏈表中有一個點:就是實現了簡單的垃圾回收重複利用棧free。這個棧的結構使每次創建的節點都來至棧頂,這時刪除的元素因爲重新入棧而達到下一次的插入位置在上一次刪除的位置使元素儘量的緊湊,這時在使用分頁虛擬存儲的計算機中能夠提高一定的訪問速度。