順序表
- 順序表: 空間連續、支持隨機訪問、物理上是連續的
- 概念:順序表是用一段物理地址連續的存儲單元依次存儲數據元素的線性結構,一般情況下采用數組存儲。在數組上完成數據的增刪查改。
- 順序表一般可以分爲:
靜態順序表:使用定長數組存儲
動態順序表:使用動態開闢的數組存儲
// 順序表的靜態存儲
#define N 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType array[N]; // 定長數組
size_t size; // 有效數據的個數
}SeqList;
// 順序表的動態存儲
typedef struct SeqList
{
SLDataType* array; // 指向動態開闢的數組
size_t size ; // 有效數據個數
size_t capicity ; // 容量空間的大小
}SeqList;
- 接口實現
靜態順序表只適用於確定知道要存儲多少數據的場景。空間開闢的大小不好控制,容易浪費或不夠用。 因此,現實中基本都使用動態順序表,根據需要動態的分配空間大小。
SeqList.h
# pragma once //防止被包含多次,多次展開
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int DataType;
typedef struct SeqList
{
DataType* _array;
size_t _size;
size_t _capacity;
}SeqList;
void SeqListInit(SeqList* ps);
void SeqListDestory(SeqList* ps);
void SeqListPushBack(SeqList* ps, DataType x);
void SeqListPushFront(SeqList* ps, DataType x);
void SeqListPopBack(SeqList* ps, DataType x);
void SeqListPopFront(SeqList* ps, DataType x);
void SeqListInsert(SeqList* ps, size_t pos,
DataType x); //O(N),建議少用
void SeqListErase(SeqList* ps, size_t pos,
DataType x); //O(N),建議少用
size_t SeqListSize(SeqList* ps);
size_t SeqListFind(SeqList* ps);
DataType SeqListAt(SeqList* ps, size_t pos);
//pos位置的值
SeqList.c
#include "SeqList.h"
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
//初始化
void SeqListInit(SeqList* ps) //傳結構體指針,否則會有“傳值錯誤”
{
assert(ps); //斷言:如果傳一個空指針,會提示第幾行有問題,可以控制,反之會崩潰,很難解決
ps->_array = NULL;
ps->_size = 0;
ps->_capacity = 0;
}
//銷燬
void SeqListDestory(SeqList* ps)
{
assert(ps);
if (ps->_array)
{
free(ps->_array);
ps->_array = NULL;
ps->_size = ps->_capacity = 0;
}
}
//插入
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
//滿了的話會增容
if (ps->_size == ps->_capacity)
{
size_t newcapacity = (ps->_capacity == 0) ? 4 : ps->_capacity * 2;
//ps->_array = realloc(ps->_array, newcapacity); //會越界(可能因爲空間開少了)
ps->_array = realloc(ps->_array, sizeof(DataType)*newcapacity);
//realloc的第一個參數如果爲空的話,相當於malloc
ps->_capacity = newcapacity;
}
}
void SeqListPushBack(SeqList* ps, DataType x)
{
assert(ps);
//增容
SeqListCheckCapacity(ps);
ps->_array[ps->_size] = x;
ps->_size++;
}
//頭插:時間複雜度爲O(N):如果要插入n個數據,則爲n^2 ,一般插入情況很多,不建議使用
//將順序表裏原有的數據,從後往前依次往後挪一個位置,最終在第一個位置插入新數據即可
//記最後一個位置爲end(爲size),前一個位置爲end-1,把end-1挪到end
void SeqListPushFront(SeqList* ps, DataType x)
{
assert(ps);
size_t end = ps->_size; //最後一個位置
while (end > 0)
{
ps->_array[end] = ps->_array[end - 1];
--end;
}
ps->_array[0] = x; //插入新的數據
ps->_size++;
}
//尾刪
void SeqListPopBack(SeqList* ps)
{
assert(ps && ps->_size > 0);
--ps->_size;
}
//頭刪
//把數據從第二個(下標爲1)到最後一個位置一次往前覆蓋一個即可
void SeqListPopFront(SeqList* ps)
{
assert(ps);
/*size_t start = 0;
while (start < ps->_size-1)*/
size_t start = 1;
while (start < ps->_size)
{
ps->_array[start - 1] = ps->_array[start];
++start;
}
--ps->_size;
}
//某個位置插入一個數據:pos位置插入一個x
//先把數據從後往前挪,直至挪到pos的位置,然後插入數據
void SeqListInsert(SeqList* ps, size_t pos,
DataType x) //O(N),建議少用
{
assert(ps);
SeqListCheckCapacity(ps);
////越界(截斷:char ch = 1; 整形提升:int i = ch; ch是一個字節;int是四個字節,提升成整形,高位補東西(此時要看高位是什麼,高位是0(1),全補0(1))),一般發生在賦值階段或者比較的時候(比較時通常是提升)
//size_t end = ps->_size - 1;
//while (end >= pos)
//{
// ps->_array[end + 1] = ps->_array[end];
// end--;
//}
//ps->_array[pos] = x;
//ps->_size++;
//int end = ps->_size - 1;
//while (end >= (int)pos) //end是int型,有符號,pos是無符號;類型不一樣時往表示範圍大是類型提升(比如:char和int,char—>int轉)//這裏會先隱式的轉爲int型
//{
// ps->_array[end + 1] = ps->_array[end];
// end--;
//}
size_t end = ps->_size;
while (end > pos) //end是int型,有符號,pos是無符號;類型不一樣時往表示範圍大是類型提升(比如:char和int,char—>int轉)//這裏會先隱式的轉爲int型
{
ps->_array[end] = ps->_array[end-1];
end--;
}
ps->_array[pos] = x;
ps->_size++;
}
size_t SeqListSize(SeqList* ps)
{
assert(ps);
return ps->_size;
}
size_t SeqListFind(SeqList* ps, DataType x);
//pos位置的值,可以用來訪問數據
DataType SeqListAt(SeqList* ps, size_t pos)
{
assert(ps);
return ps->_array[pos];
}
缺點:
1、在頭部或者中間插入、刪除數據時,效率很低,(需要挪數據,然後插入或者覆蓋),刪除時間複雜度是O(N)
2、增容
代價大(開一個更大的空間,再拷貝過去,釋放舊空間)
浪費空間(兩倍增長:100個數據,若需要插入第101個數據,就會浪費99的空間)
鏈表
-
概念:鏈表是一種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。
-
鏈表物理上不是連續的,獨立的,用指針可以鏈起來
-
鏈表有數據域、指針域(鏈接下一個位置),直至指針爲空即結束,前一個節點存儲的是下一個節點的地址 ; 用一個申請一個,沒有空間浪費
-
單向鏈表 :前面一個節點可以找到後一個節點,後一個節點不能找到前一個節點
劣勢 :從後往前不好找,以節點爲單位存儲,不支持隨機訪問 -
雙向鏈表
不帶頭:從第一個節點開始就是有效節點
帶頭:第一個節點佔位,接下來纔是有效節點
循環、不循環… (八種) -
無頭單向非循環鏈表: 結構簡單,一般不會單獨用來存數據。實際中更多是作爲其他數據結構的子結構,如哈希桶、圖的鄰接表等等。另外這種結構在筆試面試中出現很多
-
帶頭雙向循環鏈表: 結構最複雜,一般用在單獨存儲數據。實際中使用的鏈表數據結構,都是帶頭雙向循環鏈表
尾插
- 先開闢一個新的空間存儲將要插入的節點(newnode),然後判斷待插入的節點(plt)是否爲空,若爲空,找第一個節點_head,直接把plt->_head指向新節點即可;若不爲空,找最後一個節點,引入節點cur,cur去找鏈表plt的最後一個節點,沒找到之前,cur依次往後找,cur = cur->_next ;直至cur->_next == NULL 時就找到了最後一個節點,再把最後的節點指向新節點(newnode)即可
void SListPushBack(SList* plt, SLTDateType x)
{
assert(plt);
//先申請內存空間
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
newnode->_data = x;
newnode->_next = NULL;
//1.爲空
if (plt->_head == NULL)
{
plt->_head = x;
}
//2.不爲空
else
{
SListNode* cur = plt->_head;
while (cur->_next != NULL)
{
cur = cur->_next;
}
cur->_next = newnode;
}
}
頭插
- 注意插入新節點的時候,不能直接用頭指向新節點,如果這麼幹的話,那麼頭節點裏原先存儲的第一個節點的地址就找不到了,因此頭插時一定要注意這一點!!!
- 先將新節點的_next指向原來的第一個節點的地址(頭節點存的就在原來的第一個節點的地址);在把頭節點指向新節點即可
void SListPushFront(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
newnode->_data = x;
newnode->_next = NULL;
//空不空均如此
newnode->_next = plt->_head; //第一個節點的地址存儲在頭裏(plt->_head)
plt->_head = newnode;
}
頭刪
- 設cur指向第一個節點、cur->_next指向第二個節點;將_head指向第二個節點(cur->_next),把第一個節點刪除即可
void SListPopFront(SList* plt, SLTDateType x)
{
assert(plt);
if (plt == NULL)
{
return;
}
else
{
SListNode* cur = plt->_head;
plt->_head = cur->_next;
free(cur);
cur = NULL;
}
}
尾刪
+找尾:cur->_next->_next == NULL (不能直接將最後一個置爲空,這樣容易造成原鏈表倒數第二個節點的_next存在野指針的問題)
- 如果鏈表爲空,則沒什麼可刪的,直接返回即可;
- 如果鏈表只有一個節點,則刪除這個節點即可,然後把plt->_next置爲空,防止野指針
- 如果有多個節點,則可以藉助cur->_next->_next找到最後一個節點,刪除最後一個節點,再把倒數第二個節點的_next置爲空,防止野指針
void SListPopBack(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* cur = plt->_head;
//爲空節點
if (cur == NULL)
{
return;
}
//只有一個節點,剛好刪除它
else if (cur->_next == NULL)
{
free(cur);
plt->_head = NULL;
}
//多個節點
else
{
while (cur->_next->_next != NULL)
{
cur = cur->_next;
}
free(cur->_next);
cur->_next = NULL;
}
}
//C++庫裏,雙向鏈表-list/單鏈表-forward_list
//單鏈表的頭插頭刪時間複雜度均爲O(1),這是常用的,比如哈希表裏的應用
//單鏈表缺陷偏多,出的題目比較多,陷阱多一些
//頭插用雙向鏈表更容易解決;單鏈表常用於在節點的後面插入
void SListFind(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* cur = plt->_head;
while (cur != NULL)
{
if (cur->_data = x)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
//newnode得先指向pos的下一個位置,然後pos再指向newnode
void SListInsertAfter(SListNode* pos,
SLTDateType x)
{
assert(pos);
SListNode* newnode =
(SListNode*)malloc(sizeof(SListNode));
newnode->_data = x;
newnode->_next = NULL;
newnode->_next = pos->_next;
pos->_next = newnode;
}
void SListEraseAfter(SListNode* pos)
{
assert(0);
if (pos->_next == NULL)
{
return;
}
else
{
SListNode* next = pos->_next;
pos->_next = next->_next;
free(next);
next = NULL;
}
}
//刪除目標值
void SListRemove(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* prev = NULL;
SListNode* cur = plt->_head;
while (cur != NULL)
{
if (cur->_data == x)
{
if (prev == NULL) //頭刪(cur->_data == x)
{
plt->_head = cur->_next;
}
prev->_next = cur->_next;
free(cur);
cur = NULL;
return;
}
else
{
prev = cur;
cur = cur->_next;
}
}
}
【小結】
-
鏈表:以節點爲單位存儲,不支持隨機訪問,從後往前不好找。 任意位置插入刪除時間複雜度爲O(1)
-
鏈表是一種常見的基礎數據結構,是一種線性表,但是並不會按線性的順序存儲數據,而是在每一個節點的裏存到下一個節點的指針。
-
由於不是必須按照順序存儲的,鏈表在插入的時候可以達到O(1)的複雜度,比另一種線性表——順序錶快多,但是查找一個節點或者訪問特定編號的節點則需要O(N)的時間,而順序表相應的時間複雜度分別是O(log n)、O(1)
-
鏈表結構可以克服數組鏈表預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理,但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大
-
鏈表允許插入和移除表上任意位置上的節點,但是不允許隨機存取。鏈表有很多種不同類型:單向鏈表、雙向鏈表、循環鏈表
-
鏈表還可以衍生出循環鏈表、靜態鏈表、雙鏈表等。對於鏈表使用,需要注意頭結點的使用。