第二章 順序表和鏈表
順序表
什麼是順序表
順序表是物理地址全部連續的存儲數據的方式。順序表分爲動態順序表以及動態的順序表,靜態的順序表一般很少使用,因爲其大小一旦固定不能再進行改變。
順序表在開發中十分經常使用,因爲其方便簡單,並且易於操作。數組就是順序表的一種,因爲其在邏輯結構上是線性的,在物理結構上是連續的。由於十分常用在Cpp的STL庫中封裝了以順序表爲數據結構的vector
容器供我們使用。
實現
順序表的實現十分類似於vector
類的實現。
#include <iostream>
#include <stdlib.h>
#include <assert.h>
#include <stdlib.h>
#include <Windows.h>
using namespace std;
#define N 100
//順序表的實現
//十分類似於我們實現過的vector類,畢竟vector就是順序表
template<class T>
class SeqList
{
public:
//構造函數
SeqList()
:_array(nullptr)
,_size(0)
,_capacity(0)
{}
//析構函數
~SeqList()
{
if(_array != nullptr)
{
delete _array;
}
}
//pos前插入
void Insert(size_t pos, const T& value)
{
//判斷pos是否合法
if(pos > _size || pos < 0)
{
return;
}
//擴容
Expend();
for(size_t i = _size; i > pos; i--)
{
_array[i] = _array[i - 1];
}
_array[pos] = value;
_size++;
}
//尾插
void Push_back(const T& value)
{
//擴容
Expend();
_array[_size] = value;
_size++;
}
//頭插
void Push_front(const T& value)
{
//擴容
Expend();
//所有元素向後挪一個位置
for(size_t i = _size; i > 0; i--)
{
_array[i] = _array[i - 1];
}
_array[0] = value;
_size++;
}
//尾刪
void Pop_back()
{
//刪除前要先判斷,有元素才能刪
if(_size > 0)
{
_size--;
}
}
//頭刪
void Pop_front()
{
//有元素才能刪
if(_size > 0)
{
for (size_t i = 1; i < _size; i++)
{
_array[i - 1] = _array[i];
}
_size--;
}
}
//刪除pos當前位置數據
void Erase(size_t pos)
{
//pos不合法
if(pos >= _size || pos < 0)
{
return;
}
for(size_t i = pos; i < _size - 1; i++)
{
_array[pos] = _array[pos + 1];
}
_size--;
}
//查找
size_t Find(const T& value)
{
for(size_t i = 0; i < _size; i++)
{
if(_array[i] == value)
{
return i;
}
}
return -1;
}
//二分查找
size_t BinaryFind(const T& value)
{
//左閉右開區間
size_t high = _size, low = 0;
while(high > low)
{
size_t mid = (high + low) / 2;
if(_array[mid] == value)
{
return mid;
}
else if(_array[mid] > value)
{
high = mid;
}
else
{
low = mid + 1;
}
}
}
//修改
void Modify(size_t pos, const T& value)
{
if(pos < 0 || pos >= _size)
{
return;
}
_array[pos] = value;
}
//打印
void Print()
{
for(size_t i = 0; i < _size; i++)
{
cout << _array[i] << " ";
}
cout << endl;
//cout << _size << endl;
}
//當前元素個數
size_t Size()
{
return _size;
}
//某個位置的值
T& operator[](size_t pos)
{
assert(pos >=0 && pos < _size);
return _array[pos];
}
//冒泡排序
void BubbleSort()
{
for(int i = 0; i < _size - 1; i++)
{
bool flag = false;
for(int j = 0; j < _size - i - 1; j++)
{
if(_array[j] > _array[j + 1])
{
swap(_array[j], _array[j + 1]);
flag = true;
}
}
if(flag == false)
{
break;
}
}
}
void RemoveAll()
{
_size = 0;
}
private:
//T _array[N];//靜態順序表,利用數組,不可變,十分不靈活
T* _array;//動態順序表,利用指針動態開闢
size_t _size;//長度
size_t _capacity;//容量
//擴容
void Expend()
{
if(_size == _capacity)//滿了
{
size_t newCapacity = (_capacity == 0 ? 5 : 2 * _capacity);
//創建更大空間,拷貝,釋放原有空間
_array = (T*)realloc(_array, newCapacity * sizeof(T));
//申請失敗
assert(_array);
//更新容量
_capacity = newCapacity;
}
}
};
順序表的優缺點
順序表優點:
1、根據下標隨機訪問時間複雜度O(1)。
2、不會產生內存碎片。
3、代碼簡單。
4、在尾插時事件複雜度爲O1
。
順序表缺點:
1、在中間插入時時間複雜度爲On
,最壞情況下要移動整個順序表完成頭插。
2、增容申請新空間進行數據拷貝再釋放舊空間,有這不小消耗。
3、增容一次空間爲原有空間兩倍,可能會造成空間大量浪費。
鏈表
什麼是鏈表
順序表是物理地址連續的數據存儲方式,而鏈表與之相反,鏈表存儲數據的物理地址不一定連續,因此它不能像順序表那樣通過直接尋址的方式訪問到數據,它通過指針來連接和組織數據,因此也使得它和順序表有着截然不同的特徵,並且相比順序表來說或許更難理解一些。
鏈表有着以下幾種種類:
1、帶頭鏈表,不帶頭鏈表。
2、單向鏈表,雙向鏈表。
3、循環鏈表,不循環鏈表。
他們組合搭配起來一共有8種組合,
由於鏈表的特性它可以很好地結局一些順序表的缺點,比如他不需要擴容,並且更方便插入等等,但是在一些方面上它也有着不如順序表的地方,關於優缺點我們放在最後再進行分析,接下來實現一個簡單的帶頭單向不循環鏈表。
實現
在Cpp的STL中有一個list
容器,其中實現了一個帶頭雙向循環鏈表,這裏我們簡化進行實現帶頭單向不循環鏈表,思路更加簡單(帶頭雙向循環鏈表在Cpp章節中在模擬實現list
時也有實現)。
#include <iostream>
using std::cout;
using std::endl;
template<class T>
struct ListNode
{
//構造函數
ListNode(const T& value = T())
:data(value)
,next(nullptr)
{
}
T data;//數據域
ListNode* next;//指針域,指向下一個節點
};
template<class T>
class List
{
public:
//構造函數
List()
:_head(new ListNode<T>)
,_size(0)
{
}
~List()
{
//依次刪除所有節點以釋放所有空間
while(_size > 0)
{
Pop_Front();
}
delete _head;
}
//頭插
void Push_Front(const T& value)
{
InsertPos(0, value);
}
//尾插
void Push_Back(const T& value)
{
InsertPos(_size, value);
}
//打印所有數據
void PrintAll()
{
ListNode<T>* temp = _head->next;
while(temp != nullptr)
{
cout << temp->data << " ";
temp = temp->next;
}
cout << endl;
}
//在下標爲pos的元素前進行插入
void InsertPos(size_t pos, const T& value)
{
if(pos < 0 || pos > _size)
{
cout << "InsertPos: pos error" << endl;
return;
}
//這裏的插入時間複雜度本應是O1,但是由於鏈表不便於尋址因此又需要線性的時間進行尋址
ListNode<T>* node = FindForPos(pos);
if(node == nullptr)
{
return;
}
ListNode<T>* newNode = new ListNode<T>(value);
newNode->next = node->next;
node->next = newNode;
_size++;
}
//刪除下標爲pos的元素
void ErasePos(size_t pos)
{
if(pos < 0 || pos >= _size)
{
cout << "ErasePos: pos error" << endl;
return;
}
ListNode<T>* node = FindForPos(pos);
if(node == nullptr)
{
return;
}
ListNode<T>* temp = node->next;
node->next = temp->next;
delete temp;
_size--;
}
//尾刪
void Pop_Back()
{
ErasePos(_size - 1);
}
//頭刪
void Pop_Front()
{
ErasePos(0);
}
//返回鏈表長度
size_t Size()
{
return _size;
}
private:
//返回下標爲pos的元素的前一個元素,下標爲0的元素則返回頭節點
ListNode<T>* FindForPos(size_t pos)
{
ListNode<T> *temp = _head;
for(int i = 0; i < pos; i++)
{
//不存在,長度不夠
if(temp == nullptr)
{
cout << "FindForPos: pos error" << endl;
return nullptr;
}
temp = temp->next;
}
return temp;
}
private:
ListNode<T>* _head;
size_t _size;
};
鏈表的優缺點
鏈表的優點:
1、鏈表在任意位置插入和刪除時時間複雜度都能達到O1
。
2、插入一個節點開闢一個空間,不牽扯擴容問題。
鏈表的缺點:
1、以節點爲單位存儲數據,並且還要存儲指針可能會浪費更多空間。
2、不支持隨機訪問,因此在某一位置插入儘管插入只需要O1
但是找到這個位置需要On
。
3、思路實現略複雜。
鏈表反轉問題
給一個單鏈表,反轉這個單鏈表。
https://leetcode-cn.com/problems/reverse-linked-list/description/
反轉鏈表有很多種方式,我們可以尾刪所有結點並同時將他們重新頭插回鏈表,但這樣的思路消耗實在太高,我們每次尾插都不得不遍歷整張單鏈表去找到尾,因此對其簡化,這裏提供兩種思路,
第一種,後插頭刪法。這種方法是通過從第二個節點開始遍歷整個鏈表,依次將節點移向頭部的方法。圖解如下:
題解:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == NULL)
{
return head;
}
ListNode* oldHead = head;
ListNode* temp = oldHead->next;
while(oldHead->next != NULL)
{
oldHead->next = temp->next;
temp->next = head;
head = temp;
temp = oldHead->next;
}
return head;
}
};
2、第二種:向後轉法。第二種思路更爲直觀,我們就直接將每個結點中的next
的指向改變即可。
題解:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == NULL)
{
return head;
}
ListNode* prev = head;
ListNode* cur = head->next;
ListNode* next = cur;
head->next = NULL;
while(cur != NULL)
{
next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
};
鏈表有多種多樣的問題,我會另開章節逐個歸納,就不再此繼續羅列了。