c语言中指针的作用不言而喻,指针是c语言的一大特色。理解指针的并不是特别难,但是理解二级指针却并不是那么简单。下面就开发中常见的二级指针的场景总结一下。
一, 类似二维数组的用法
在hash表算法中一般会用二级指针来存储元素。说存储也不是很准确,因为指针并没有存储对象,只是存储了对象的地址。hash表为什么会用到二级指针呢?因为hash表会存在冲突的问题,每个槽中要有分支,来存储冲突的元素,以下是hash表示的示意图:
可以看到每个槽可能会有多个元素,所以必须用二维数组来存储元素,由于二维数组的大小必须提前,用二级指针可以动态指定大小。
下面给出了一段简单的hash表算法的代码:
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#define UINT unsigned int
#define SIZE_T unsigned int
struct TITEM
{
char* Key;
void* Data;
struct TITEM* pPrev;
struct TITEM* pNext;
};
class HashMap
{
public:
HashMap(int nSize = 83);
~HashMap();
void Resize(int nSize = 83);
void* Find(char* key, bool optimize = true) const;
bool Insert(char* key, void* pData);
void* Set(char* key, void* pData);
bool Remove(char* key);
void RemoveAll();
int GetSize() const;
char* GetAt(int iIndex) const;
char* operator[] (int nIndex) const;
protected:
TITEM** m_aT;
int m_nBuckets;
int m_nCount;
};
static UINT HashKey(char* Key)
{
UINT i = 0;
SIZE_T len = strlen(Key);
while( len-- > 0 ) i = (i << 5) + i + Key[len];
return i;
}
static UINT HashKey(const char* Key)
{
return HashKey((char*)Key);
};
HashMap::HashMap(int nSize) : m_nCount(0)
{
if( nSize < 16 ) nSize = 16;
m_nBuckets = nSize;
m_aT = new TITEM*[nSize];
memset(m_aT, 0, nSize * sizeof(TITEM*));
}
HashMap::~HashMap()
{
if( m_aT ) {
int len = m_nBuckets;
while( len-- ) {
TITEM* pItem = m_aT[len];
while( pItem ) {
TITEM* pKill = pItem;
pItem = pItem->pNext;
delete pKill;
}
}
delete [] m_aT;
m_aT = NULL;
}
}
void HashMap::RemoveAll()
{
this->Resize(m_nBuckets);
}
void HashMap::Resize(int nSize)
{
if( m_aT ) {
int len = m_nBuckets;
while( len-- ) {
TITEM* pItem = m_aT[len];
while( pItem ) {
TITEM* pKill = pItem;
pItem = pItem->pNext;
delete pKill;
}
}
delete [] m_aT;
m_aT = NULL;
}
if( nSize < 0 ) nSize = 0;
if( nSize > 0 ) {
m_aT = new TITEM*[nSize];
memset(m_aT, 0, nSize * sizeof(TITEM*));
}
m_nBuckets = nSize;
m_nCount = 0;
}
void* HashMap::Find(char* key, bool optimize) const
{
if( m_nBuckets == 0 || GetSize() == 0 ) return NULL;
UINT slot = HashKey(key) % m_nBuckets;
for( TITEM* pItem = m_aT[slot]; pItem; pItem = pItem->pNext ) {
if( pItem->Key == key ) {
if (optimize && pItem != m_aT[slot]) {
if (pItem->pNext) {
pItem->pNext->pPrev = pItem->pPrev;
}
pItem->pPrev->pNext = pItem->pNext;
pItem->pPrev = NULL;
pItem->pNext = m_aT[slot];
pItem->pNext->pPrev = pItem;
m_aT[slot] = pItem;
}
return pItem->Data;
}
}
return NULL;
}
bool HashMap::Insert(char* key, void* pData)
{
if( m_nBuckets == 0 ) return false;
if( Find(key) ) return false;
// Add first in bucket
UINT slot = HashKey(key) % m_nBuckets;
TITEM* pItem = new TITEM;
pItem->Key = key;
pItem->Data = pData;
pItem->pPrev = NULL;
pItem->pNext = m_aT[slot];
if (pItem->pNext)
pItem->pNext->pPrev = pItem;
m_aT[slot] = pItem;
m_nCount++;
return true;
}
void* HashMap::Set(char* key, void* pData)
{
if( m_nBuckets == 0 ) return pData;
if (GetSize()>0) {
UINT slot = HashKey(key) % m_nBuckets;
// Modify existing item
for( TITEM* pItem = m_aT[slot]; pItem; pItem = pItem->pNext ) {
if( pItem->Key == key ) {
void* pOldData = pItem->Data;
pItem->Data = pData;
return pOldData;
}
}
}
Insert(key, pData);
return NULL;
}
bool HashMap::Remove(char* key)
{
if( m_nBuckets == 0 || GetSize() == 0 ) return false;
UINT slot = HashKey(key) % m_nBuckets;
TITEM** ppItem = &m_aT[slot];
while( *ppItem ) {
if( (*ppItem)->Key == key ) {
TITEM* pKill = *ppItem;
*ppItem = (*ppItem)->pNext;
if (*ppItem)
(*ppItem)->pPrev = pKill->pPrev;
delete pKill;
m_nCount--;
return true;
}
ppItem = &((*ppItem)->pNext);
}
return false;
}
int HashMap::GetSize() const
{
#if 0//def _DEBUG
int nCount = 0;
int len = m_nBuckets;
while( len-- ) {
for( const TITEM* pItem = m_aT[len]; pItem; pItem = pItem->pNext ) nCount++;
}
ASSERT(m_nCount==nCount);
#endif
return m_nCount;
}
char* HashMap::GetAt(int iIndex) const
{
if( m_nBuckets == 0 || GetSize() == 0 ) return false;
int pos = 0;
int len = m_nBuckets;
while( len-- ) {
for( TITEM* pItem = m_aT[len]; pItem; pItem = pItem->pNext ) {
if( pos++ == iIndex ) {
return pItem->Key;
}
}
}
return NULL;
}
char* HashMap::operator[] (int nIndex) const
{
return GetAt(nIndex);
}
int main() {
HashMap map_;
map_.Insert("hello", (void*)"world");
char* str = (char*)map_.Find("hello");
printf("str is %s\n", str);
}
二, 特殊用法,改变链表的指向
对于链表,常用的用法有插入和删除算法。通常插入,删除算法的套路是声明两个指针变量,分别指向目标节点和前一节点,然后改变节点指向。这种算法思路清晰明了,编码也简单,考虑边缘的情况(例如插入,删除在头部)也不是什么难事。
有种更巧妙的算法,直接在链表节点上修改节点指向。但是这种算法不太直接,理解起来不是很容易。例如插入元素,原本元素和将要插入的元素如下表:
要插入的元素介于a,b之间,按照一般的算法,我们会用两个变量来保存这两个值。首先让d指向b的地址,然后让a指向d的地址,变为如下:
其实一个变量就可以搞定,因为他们是有联系的,b的地址是a变量的一个值。一个变量即可表示b的地址,也可以指示a的值,他就是二级数组。以下三行代码就可以表示这一过程:
Node** pNode = &pNodeA; //指向nodeA地址的地址,可能为0x334
pNodeD->next = *pNodeB; //pNode->next = 0x320
*pNode = pNodeD //改变了A的指向
删除节点也是同样的道理。下面给出完整算法:
Node* insert(Node* pH, Node* newData) {
Node** link = &pH;
while(*link && (*link)->data < newData->data) {
link = &(*link)->next;
}
newData->next = *link;
*link = newData;
return pH;
}
Node* delNode(Node* pH, int data) {
Node** link = &pH;
while(*link && (*link)->data != data) {
link = &(*link)->next;
}
if (*link) { //确保删除不存在的元素时不为空
*link = (*link)->next;
}
return pH;
}
和上一种删除的算法对比一下:
Node* delNode(Node* pH, int data) {
Node* pCur = pH;
Node* pPre = pCur;
while(pCur && pCur->data != data) {
pPre = pCur;
pCur = pCur->next;
}
if (pH == pCur) { //删除的是头节点
pH = pCur->next;
}
if (pCur) { //确保删除不存在的元素时不为空
pPre->next = pCur->next;
}
return pH;
}
参看:
https://coolshell.cn/articles/8990.html
https://github.com/duilib/duilib/blob/master/DuiLib/UIlib.cpp