说在前面
个人读书笔记
向量和列表
最为基本的线性结构统称为序列(sequence),根据其中数据项的逻辑次序与其物理存储地址的对应关系不同,又可进一步地将序列区分为向量(vector)和列表(list)。
在向量中,所有数据项的物理存放位置与其逻辑次序完全吻合,此时的逻辑次序也称作秩(rank);而在列表中,逻辑上相邻的数据项在物理上未必相邻,而是采用间接定址的方式通过封装后的位置(position)相互引用。
向量
若集合S由n个元素组成,且各元素之间具有一个线性次序,则可将它们存放于起始于地址A、物理位置连续的一段存储空间,并统称作数组(array)。
向量(vector)就是线性数组的一种抽象与泛化,我们不再限定同一向量中的各元素都属于同一基本类型,它们本身可以是来自于更具一般性的某一类的对象。另外,各元素也不见得同时具有某一数值属性,故而并不保证它们之间能够相互比较大小。
若向量 S [ 0 , n ) S[0, n) S[0,n)中的所有元素不仅按线性次序存放,而且其数值大小也按此次序单调分布,则称作有序向量(sorted vector)。有序向量的定义中隐含了另一更强的先决条件:各元素之间必须能够比较大小。
有序向量唯一化
低效版:
其正确性基于如下事实:
有序向量中的重复元素必然前后紧邻。于是,可以自前向后地逐一检查各对相邻元素:
若二者雷同则调用remove()接口删除靠后者,否则转向下一对相邻元素。如此,扫描结束后向量中将不再含有重复元素。
这里的运行时间,主要消耗于while循环,共需迭代_size - 1 = n - 1步。此外,在最坏情况下,每次循环都需执行一次remove()
操作。因此,时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。这一效率竟与向量未排序时相同,说明该方法未能充分利用此时向量的有序性。
高效版:
有序向量查找
二分查找:
假设在区间
S
[
l
o
,
h
i
)
S[lo, hi)
S[lo,hi)中查找目标元素
e
e
e。
以任一元素
S
[
m
i
]
=
x
S[mi] = x
S[mi]=x为界,都可将区间分为三部分,且根据此时的有序性必有:
S
[
l
o
,
m
i
)
<
=
S
[
m
i
]
<
=
S
(
m
i
,
h
i
)
S[lo, mi) <= S[mi] <=S(mi, hi)
S[lo,mi)<=S[mi]<=S(mi,hi)
于是,只需将目标元素
e
e
e与
x
x
x做一比较,即可视比较结果分三种情况做进一步处理:
- 若 e < x e <x e<x,则目标元素如果存在,必属于左侧子区间 S [ l o , m i ) S[lo, mi) S[lo,mi),故可深入其中继续查找;
- 若 x < e x < e x<e,则目标元素如果存在,必属于右侧子区间 S ( m i , h i ) S(mi, hi) S(mi,hi),故也可深入其中继续查找;
- 若 e = x e = x e=x,则意味着已经在此处命中,故查找随即终止。
时间复杂度:
O
(
l
o
g
n
)
O(logn)
O(logn)
查找算法的整体效率更主要地取决于其中所执行的元素大小比较操作的次数,即所谓查找长度(search length)。
排序与下界
一般地,任一问题在最坏情况下的最低计算成本,即为该问题的复杂度下界(lower bound)。
归并排序
理论最优时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
与起泡排序通过反复调用单趟扫描交换类似,归并排序也可以理解为是通过反复调用所谓二路归并(2-way merge)算法而实现的。所谓二路归并,就是将两个有序序列合并成为一个有序序列。这里的序列既可以是向量,也可以是列表,这里首先考虑有序向量。归并排序所需的时间,也主要决定于各趟二路归并所需时间的总和。
二路归并属于迭代式算法。每步迭代中,只需比较两个待归并向量的首元素,将小者取出并追加到输出向量的末尾,该元素在原向量中的后继则成为新的首元素。如此往复,直到某一向量为空。最后,将另一非空的向量整体接至输出向量的末尾。
二路归并示例:
归并排序示例:
c++实现数组的归并排序(自顶向下)(递归实现):
时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
#include <iostream>
using namespace std;
template <typename T>
void __merge(T arr[], int l, int mid, int r)
{
T aux[r-l+1];
for (int i = l; i<=r; i++)
{
aux[i-l] = arr[i];
}
int i = l, j = mid + 1;
for (int k = l; k<=r; k++)
{
if (i > mid)
{
arr[k] = aux[j-l];
j++;
}
else if(j > r)
{
arr[k] = aux[i-l];
i++;
}
else if(aux[i-l] < aux[j-l])
{
arr[k] = aux[i-l];
i++;
}
else
{
arr[k] = aux[j-l];
j++;
}
}
}
template <typename T>
void __mergeSort(T arr[], int l, int r)
{
if (l >= r)
{
return;
}
int mid = (l+r) / 2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid+1, r);
__merge(arr, l, mid, r);
}
template <typename T>
void mergeSort(T arr[], int n)
{
__mergeSort(arr, 0, n-1);
}
int main()
{
int a[10] = {10, 9, 8, 6, 11, 5, 4, 1, 0, 3};
mergeSort(a, 10);
for (int i = 0; i< 10; i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
c++实现数组的归并排序(自底向上)(迭代实现):
#include <iostream>
using namespace std;
template <typename T>
void __merge(T arr[], int l, int mid, int r)
{
T aux[r-l+1];
for (int i = l; i<=r; i++)
{
aux[i-l] = arr[i];
}
int i = l, j = mid + 1;
for (int k = l; k<=r; k++)
{
if (i > mid)
{
arr[k] = aux[j-l];
j++;
}
else if(j > r)
{
arr[k] = aux[i-l];
i++;
}
else if(aux[i-l] < aux[j-l])
{
arr[k] = aux[i-l];
i++;
}
else
{
arr[k] = aux[j-l];
j++;
}
}
}
template <typename T>
void mergeSortBU(T arr[], int n)
{
for (int sz = 1; sz <= n; sz = sz + sz)
{
for (int i = 0; i +sz < n; i = i + sz + sz)
{
__merge(arr, i, i+sz -1, min(i+sz+sz-1, n-1));
}
}
}
int main()
{
int a[10] = {10, 9, 8, 6, 11, 5, 4, 1, 0, 3};
mergeSortBU(a, 10);
for (int i = 0; i< 10; i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
python实现的归并排序(自顶向下)(递归实现):
# -*- coding:utf-8 -*-
class MergeSort:
def __init__(self, list0, n):
self.list0 = list0
self.n = n
@staticmethod
def _merge(list0, l, mid, r):
list1 = list0[:]
p = l
q = mid + 1
for i in range(r-l+1):
if p > mid:
list0[i+l] = list1[q]
q = q + 1
elif q > r:
list0[i+l] = list1[p]
p = p + 1
elif list1[p] < list1[q]:
list0[i+l] = list1[p]
p = p + 1
else:
list0[i+l] = list1[q]
q = q + 1
def _merge_sort(self, list0, l, r):
if l >= r:
return
mid = int((l+r)/2)
self._merge_sort(list0, l, mid)
self._merge_sort(list0, mid+1, r)
self._merge(list0, l, mid, r)
def merge_sort(self):
self._merge_sort(self.list0, 0, self.n - 1)
def main():
a = [10, 9, 8, 6, 11, 5, 4, 1, 0, 3]
b = MergeSort(a, len(a))
b.merge_sort()
print(a)
if __name__ == "__main__":
main()
列表
数据结构支持的操作,通常无非静态和动态两类:前者仅从中获取信息,后者则会修改数据结构的局部甚至整体。
以向量结构,其size()和get()等静态操作均可在常数时间内完成,而insert()和remove()等动态操作却都可能需要线性时间。究其原因,在于“各元素物理地址连续”的约定——即所谓的“静态存储”策略。
得益于这种策略,可在 O ( 1 ) O(1) O(1)时间内由秩确定向量元素的物理地址;但反过来,在添加(删除)元素之前(之后),又不得不移动 O ( n ) O(n) O(n)个后继元素。可见,尽管如此可使静态操作的效率达到极致,但就动态操作而言,局部的修改可能引起大范围甚至整个数据结构的调整。
列表(list)结构尽管也要求各元素在逻辑上具有线性次序,但对其物理地址却未作任何限制——此即所谓“动态存储”策略。
具体地,在其生命期内,此类数据结构将随着内部数据的需要,相应地分配或回收局部的数据空间。如此,元素之间的逻辑关系得以延续,却不必与其物理次序相关。作为补偿,此类结构将通过指针或引用等机制,来确定各元素的实际物理地址。
例如,链表(linked list)就是一种典型的动态存储结构。其中的数据,分散为一系列称作节点(node)的单位,节点之间通过指针相互索引和访问。为了引入新节点或删除原有节点,只需在局部,调整少量相关节点之间的指针。这就意味着,采用动态存储策略,至少可以大大降低动态操作的成本。
列表是链表结构的一般化推广,其中的元素称作节点(node),分别由特定的位置或链接指代。
双向链表插入节点:
双向链表删除节点:
有序列表唯一化
与有序向量同理,有序列表中的雷同节点也必然(在逻辑上)彼此紧邻。利用这一特性,可实现重复节点删除算法如下图所示。位置指针p和q分别指向每一对相邻的节点,若二者雷同则删除q,否则转向下一对相邻节点。如此反复迭代,直至检查过所有节点。
有序列表查找
尽管有序列表中的节点已在逻辑上按次序单调排列,但在动态存储策略中,节点的物理地址与逻辑次序毫无关系,故无法像有序向量那样自如地应用减治策略,从而不得不继续沿用无序列表的顺序查找策略。
排序
插入排序
理论最优时间复杂度: O ( n 2 ) O(n^2) O(n2)
插入排序(insertionsort)算法适用于包括向量与列表在内的任何序列结构。算法的思路可简要描述为:
始终将整个序列视作并切分为两部分:有序的前缀,无序的后缀;通过迭代,反复地将后缀的首元素转移至前缀中。由此亦可看出插入排序算法的不变性:
在任何时刻,相对于当前节点
e
=
S
[
r
]
e = S[r]
e=S[r],前缀
S
[
0
,
r
)
S[0, r)
S[0,r)总是业已有序。
c++实现数组的插入排序:
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
#include <iostream>
using namespace std;
template <typename T>
void insertionSort(T arr[], int n)
{
for (int i = 1; i < n; i ++)
{
for (int j = i; j > 0; j --)
{
if (arr[j] < arr[j-1])
{
swap(arr[j], arr[j-1]);
}
else
{
break;
}
}
}
}
int main()
{
int a[10] = {10, 9, 8, 6, 11, 5, 4, 1, 0, 3};
insertionSort(a, 10);
for (int i = 0; i< 10; i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
python实现的插入排序:
# -*- coding:utf-8 -*-
def insertion_sort(list0, n):
for i in range(1, n):
for j in range(0, i):
if list0[i-j] < list0[i-j-1]:
b = list0[i-j]
list0[i-j] = list0[i-j-1]
list0[i-j-1] = b
else:
break
print(list0)
def main():
a = [10, 9, 8, 6, 11, 5, 4, 1, 0, 3]
insertion_sort(a, len(a))
if __name__ == "__main__":
main()
选择排序
理论最优时间复杂度(借助更高级的数据结构): O ( n l o g n ) O(nlogn) O(nlogn)
选择排序(selectionsort)也适用于向量与列表之类的序列结构。
与插入排序类似,该算法也将序列划分为无序前缀和有序后缀两部分;此外,还要求前缀不大于后后缀。如此,每次只需从前缀中选出最大者,并作为最小元素转移至后缀中,即可使有序部分的范围不断扩张。
同样地,上述描述也给出了选择排序算法过程所具有的不变性:
在任何时刻,后缀S[r, n)已经有序,且不小于前缀S[0, r)
c++实现数组的选择排序:
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
#include <iostream>
using namespace std;
template <typename T>
void selectionSort(T arr[], int n)
{
for (int i = 0; i < n; i ++)
{
int minIndex = i;
for (int j = i + 1; j < n; j ++)
{
if (arr[j] < arr[minIndex])
{
minIndex = j;
}
}
swap(arr[i], arr[minIndex]);
}
}
int main()
{
int a[10] = {10, 9, 8, 6, 11, 5, 4, 1, 0, 3};
selectionSort(a, 10);
for (int i = 0; i< 10; i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
python实现的选择排序:
# -*- coding:utf-8 -*-
def selection_sort(list0, n):
for i in range(n):
min_index = i
for j in range(i, n):
if list0[j] < list0[min_index]:
min_index = j
b = list0[i]
list0[i] = list0[min_index]
list0[min_index] = b
print(list0)
def main():
a = [10, 9, 8, 6, 11, 5, 4, 1, 0, 3]
selection_sort(a, len(a))
if __name__ == "__main__":
main()
结语
如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。