数据结构主要涉及查找、插入、删除的时间复杂度(效率)和空间复杂度(内存开销)。我拟写一系列相关文档,总结所学,做好mark,以备后查。也顺便供大家学习、参考、交流。
参考编程语言的惯例,由简至难,从数组开篇。毕竟,数组-大家耳熟能详,使用率也相当高。
数组适用场景
- 数据量不大
- 数量可预估
数组分类
- 有序数组 (数据元素按从小到大或从大到小排列)
- 无序数组
无序数组没有规律,它的查找、删除、插入只能线性进行。本文主要针对有序数组进行展开:
有序数组查找算法
二分法
大家对二分法耳熟能详,这里不罗嗦,直接贴上代码(包括递归和迭代两种编码形式)
/*二分法查找有序数组中的元素*/
/* 递归实现二分查找,查找成功返回所在数组下标,否则返回-1 */
int BinarySearchRecursion(int* pData, int nLow, int nHigh, int nVal)
{
if (nLow > nHigh)
return -1;
int nMid = (nLow + nHigh) / 2;
if (pData[nMid] == nVal)
return nMid;
if (pData[nMid] > nVal)
{
nHigh = nMid - 1;
return BinarySearchRecursion(pData, nLow, nHigh, nVal);
}
if (pData[nMid] < nVal)
{
nLow = nMid + 1;
return BinarySearchRecursion(pData, nLow, nHigh, nVal);
}
return -1;
}
/*迭代实现二分查找,查找成功返回所在数组下标,否则返回-1 */
int DoBinarySearchIterator(int* pData, int nCount, int nVal)
{
int nLow = 0, nHigh = nCount - 1, nMid = 0;
while (nLow <= nHigh)
{
nMid = (nLow + nHigh) / 2;
if (pData[nMid] == nVal)
return nMid;
if (pData[nMid] < nVal)
nLow = nMid + 1;
else/* if (pData[mid] > nVal) */
nHigh = nMid - 1;
}
return -1;
}
插值(比例)法
二分查找法把有序数组折半分片定位,那为什么不能是1/3、1/4呢?插值法则是动态调整分片比例。
/*插值查找*/
int DoInterpolationSearch(int *pData, int nCount, int nVal)
{
int nLow = 0, nHigh = nCount - 1, nMid = 0;
while(nLow <= nHigh)
{
/*套用插值公式*/
nMid = nLow + (nHigh - nLow) * (nVal - pData[nLow]) / (pData[nHigh] - pData[nLow]);
if(pData[nMid] == nVal)
return nMid;
if (pData[nMid] > nVal)
nHigh = nMid - 1;
else /*if(pData[nMid] < nVal) */
nLow = nMid + 1;
}
return -1;
}
黄金分割(斐多那契)法
先来一起复习下斐多拉契数列:
随着数列的无限增长,相邻前后两数的比例越来越接近黄金比例值。可以试着计算下:1、1、2、3、5、8、13、21、34、55、89、…
斐波那契查找利用斐波那契数列的黄金分割原理来取得中间记录作为比较对象。需要额外生成斐波那契数列,但可用查表法优化掉二分法及插值法中的除法运算。(数列中n=30时F=832040,n=50时F=12586269025)
/* 斐多那契数列,n=47(n=48时Fn大于0xffffffff), 这里直接使用常量表以去除算法中数列生成的开销 */
const unsigned int FIB[] = {1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,
4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309,
3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296,
433494437, 701408733, 1134903170, 1836311903, 2971215073 };
/*斐波那契查找*/
int DoFibonacciSearch(int* pData, int nCount, int nVal)
{
int nLow = 0, nHigh = nCount - 1, nMid = 0, nFound = -1;
/*求斐波那契数列项n*/
int n = 0;
while(nCount > FIB[n]-1)
n++;
int* tmp = pData;
if (nCount < FIB[n])
{
/*补全待查找序列*/
tmp = new int [FIB[n]];
memcpy(tmp, pData, nCount * sizeof(int));
for (int i = nCount; i < FIB[n]; i++)
tmp[i] = tmp[nHigh]; /* 补全 */
}
while(nLow <= nHigh)
{
nMid = nLow + FIB[n-1] - 1;
if (nVal < tmp[nMid])
{
nHigh = nMid - 1;
n -= 1;
}
else if(nVal > tmp[nMid])
{
nLow = nMid + 1;
n -= 2;
}
else
{
nFound = (nMid < nCount)? nMid : nCount - 1;
break;
}
}
if (tmp != pData)
{
delete [] tmp;
}
return nFound;
}
斐波那契查找算法的缺陷:待查找序列必须补全到跟斐波那契数组中的对应数的长度,要求待查找序列的存储空间要大于自身(大多少视具体情况而定)。
算法选择及时间复杂度和空间复杂度对比
- 如果数组数据近等比、等差等特殊序列,可选择插值法,可更快逼近
- 数据量相对较大时可选择斐多那契法,虽然它的时间复杂读同二分法,但减少了除法运算开销,略优。
- 三种查找算法的时间复杂度都是O(logN),但空间复杂度斐多那契法更大(额外需要斐多那契数列,很可能还需要数组补全)。