查找
在数据集合中寻找满足某种条件的元素
顺序查找法
又称线性查找,主要用于在线性表中进行查找。算法主要思想是:
- 设线性表中有
n
n
n个元素,给定一个待查找的值
t
a
r
g
e
t
target
target;
- 从表的一端开始,顺序扫描,依次将扫描到的关键码和目标值
t
a
r
g
e
t
target
target进行比较,直到当前扫描的关键码与
t
a
r
g
e
t
target
target相等,则查找成功,并获取该关键码在表中的位置;
- 整个表扫描结束后,没有发现与
t
a
r
g
e
t
target
target相等的关键码,则查找失败。
代码实现
int SeqSearch(int List[], int size, int target) {
if (size == 0) return -1;
for (int i = 0; i < size; i++) {
if (List[i] == target)
return i;
}
int* newList = (int*)malloc(sizeof(int) * (size + 1));
for (int i = 0; i < size; i++) {
newList[i] = List[i];
}
newList[size] = target;
int j = 0;
while (List[j] != target) {
j++;
}
free(newList);
return j >= size ? -1 : j;
int k = 0;
while (List[k] < target) {
k++;
}
if (List[k] == target)
return k;
else
return -1;
}
优化
- 在普通顺序表上使用监视哨,将目标值插入在数组的尾部,在查找过程中一定会找到一个与目标值相等的元素,如果该元素的位置不在最后一位,则查找成功,否则说明查找失败;
- 在有序顺序表进行顺序查找,找到第一个不小于目标值的元素与目标值进行比较,若相等则查找成功,否则查找失败,后续元素不需要进行比较。
折半查找
又称二分查找、对分查找,算法主要思想如下: ??设有
n
n
n个元素存放在一个有序的顺序表中,采用折半查找时,首先将位于查找区间正中位置的元素
n
u
m
s
[
m
i
d
]
nums[mid]
nums[mid]与目标值
t
a
r
g
e
t
target
target进行比较: ????若
n
u
m
s
[
m
i
d
]
=
=
t
a
r
g
e
t
nums[mid]==target
nums[mid]==target则查找成功,并返回元素下标; ????若
n
u
m
s
[
m
i
d
]
<
t
a
r
g
e
t
nums[mid]<target
nums[mid]<target,则将查找区间缩小至数组的前半部分,再继续进行折半查找; ????若
n
u
m
s
[
m
i
d
]
>
t
a
r
g
e
t
nums[mid]>target
nums[mid]>target,则将查找区间缩小至数组的前半部分,再继续进行折半查找; ??每比较一次,查找区间缩小一半,在最坏的情况下,查找成功所需的关键码比较次数为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2?n)。
代码实现
int BinSearch(vector<int> &nums, int target) {
if (nums.size() == 0)return -1;
int left = 0, right = nums.size() - 1, mid;
while (left <= right) {
mid = (left + right) / 2;
if (nums[mid] == target)
return mid;
if (nums[mid] < target)
right = mid - 1;
if (nums[mid] > target)
left = mid + 1;
}
return -1;
}
分块查找
又称索引顺序查找,主要思想是将一个大的线性表分解成若干块,每块中的结点可以任意存放(无序排列),但块与块之间必须进行排序,即对于任意
i
i
i,第
i
i
i块中的所有结点的关键码都必须小于(或大于)第
i
+
1
i+1
i+1块中的所有结点的关键码。
此外,还要建立一个索引表,把每块中的最大关键码作为索引表的关键码,按块的顺序存放在一个数组中,这个数组是按关键码排序的。
查找时,先在索引表中进行查找,确定目标结点所在的块,然后在相应的块中进行顺序查找,即可找到相应的结点。
查找小结
查找方法 | 基本思想 | 时间复杂度 |
---|
顺序查找/线性查找 | 从头开始依次遍历所有元素 |
O
(
n
)
O(n)
O(n) | 折半查找/二分查找 | 针对有序表,率先比较中间位置元素,进而缩小搜索空间 |
O
(
l
o
g
n
)
O(logn)
O(logn) | 分块查找/索引顺序查找 | 块内无序、块间有序 |
O
(
l
o
g
(
b
+
1
)
+
(
s
+
1
)
/
2
)
O(log(b+1)+(s+1)/2)
O(log(b+1)+(s+1)/2) |
内部排序
直接插入排序
??把数组
a
[
n
]
a[n]
a[n]中待排序的
n
n
n个元素看成一个有序表(
a
[
0
]
a[0]
a[0])和一个无序表(
a
[
1
]
,
a
[
2
]
,
.
.
.
,
a
[
n
?
1
]
a[1],a[2],...,a[n-1]
a[1],a[2],...,a[n?1])。排序过程中每次将无序表中的第一个元素插入有序表的适当位置,使之成为新的有序表,经过
n
?
1
n-1
n?1次插入后,无序表中的元素全部插入到有序表中,原数组
a
[
n
]
a[n]
a[n]排序完成。
代码实现
void InsertSort(int List[], int size) {
int temp;
int i, j;
for (i = 1; i < size; i++) {
if (List[i] < List[i - 1]) {
temp = List[i];
for (j = i - 1; List[j] > temp && j >= 0; j--) {
List[j + 1] = List[j];
}
List[j + 1] = temp;
}
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1),稳定排序。
折半插入
与直接插入的思想相同,只是在查找插入位置时,选择折半查找法寻找待插入位置。
代码实现
void BinaryInsertSort(int List[], int size) {
int tmp;
int i, j, left, right, mid;
for (i = 1; i < size; i++) {
if (List[i] < List[i - 1]) {
tmp = List[i];
left = 0;
right = i - 1;
while (left <= right) {
mid = (left + right) / 2;
if (tmp < List[mid])
right = mid - 1;
else
left = mid + 1;
}
for (j = i - 1; j >= left; j--) {
List[j + 1] = List[j];
}
List[left] = tmp;
}
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1),稳定排序。
折半插入排序过程中的比较次数与待排序元素的初始序列无关,但是元素移动次数与待排序元素的初始位置有关。
希尔排序
又称缩小增量排序,排序思想为:
- 设待排序元素序列有
n
n
n个元素,首先取一个整数
g
a
p
<
n
gap<n
gap<n作为间隔,将全部元素分为
g
a
p
gap
gap个子序列;
- 所有距离为
g
a
p
gap
gap的元素放入同一个子序列中,对每个子序列分别进行直接插入排序;
- 然后缩小
g
a
p
gap
gap,重复上述子序列的划分和排序,直到
g
a
p
=
1
gap=1
gap=1,将所有元素放在同一序列中为止。
代码实现
void InsertSort_gap(vector<int>& nums, int start, int gap) {
int tmp;
int i, j;
for (i = start + gap; i < nums.size(); i = i + gap) {
if (nums[i - gap] > nums[i]) {
tmp = nums[i];
j = i;
do {
nums[j] = nums[j - gap];
j -= gap;
} while (j - gap > 0 && nums[j - gap] > tmp);
nums[j] = tmp;
}
}
}
void ShellSort(vector<int>& nums) {
int start, gap = nums.size();
while (gap != 1) {
gap = ceil((double)gap / 2);
for (start = 0; start < gap; start++) {
InsertSort_gap(nums, start, gap);
}
}
}
时间复杂度介于
O
(
n
)
O(n)
O(n)和
O
(
n
2
)
O(n^2)
O(n2)之间,空间复杂度
O
(
1
)
O(1)
O(1),不稳定排序。
简单选择排序
又称直接选择排序,主要思想为:
- 设待排序序列有
n
n
n个元素;
- 第一趟排序从待排序序列中选取关键码最小的元素,若该元素不位于第一个位置,则与待排序序列的第一个元素交换,并将其从待排序序列中删除;
- 第一趟排序从剩下的待排序序列中选择关键码最小的元素,重复上述过程,通过
n
?
1
n-1
n?1趟排序后得到有序序列。
代码实现
void SelectSort(vector<int>& nums) {
int tmp;
int i, j, k;
for (i = 0; i < nums.size(); i++) {
k = i;
for (j = i + 1; j < nums.size(); j++) {
if (nums[j] < nums[k]) {
k = j;
}
}
if (k != i) {
tmp = nums[i];
nums[i] = nums[k];
nums[k] = tmp;
}
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1),不稳定排序。
锦标赛排序
按照锦标赛的思想进行排序,其算法思想如下:
- 首先对
n
n
n个元素的排序码进行两两比较,得到
?
n
/
2
?
\lceil n/2 \rceil
?n/2?个优胜者;
- 对优胜者再进行两两比较,如此重复,直到选择具有最小的排序码元素为止。
堆排序
- 将一个无序序列调整为一个大根堆(或是小根堆),找出这个序列的最大(或最小)值,移动到有序序列的末尾位置;
- 对剩下的无序序列重复调整为一个大根堆(或是小根堆),重复以上过程,直到无序序列中没有元素。
冒泡排序
又称起泡排序,主要思想为:
- 对待待排序元素序列从后向前,一次比较相邻元素的排序码,若发现存在逆序则交换,使排序码较小的元素逐渐从下标较大的位置移向下标较小的位置。
- 一趟排序下来,排序码最小的元素就被交换到了待排序元素序列的第一个位置;
- 下一趟排序时,已确定位置的元素不再参与排序;
优化
??在冒泡排序过程中,各元素不断接近自己的最终位置,如果一趟比较下来没有进行过交换操作,则说明待排序序列有序,因此可以设置一个标志,记录元素是否进行过交换,从而减少不必要的比较。
代码实现
void BubbleSort(vector<int>& nums) {
int exchange;
int i, j;
int tmp;
for (i = 0; i <= nums.size() - 2; i++) {
exchange = 0;
for (j = nums.size() - 1; j >= i + 1; j--) {
if (nums[j - 1] > nums[j]) {
tmp = nums[j - 1];
nums[j - 1] = nums[j];
nums[j] = tmp;
exchange = 1;
}
}
if (!exchange) return;
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1),稳定排序
快速排序
- 任意取待排序元素序列中的某个元素作为基准元素,一般取第一个元素,按照基准元素的排序码大小,将整个待排序元素序列分为左右两个子序列:左子序列中所有元素的排序码都小于基准元素的排序码,右子序列中所有元素的排序码都大于基准元素的排序码,基准元素处于最终位置。
- 分别对两个子序列重复上述方法各自划分成两个更小的子序列,直到最后划分出的子序列最多只有一个元素为止。
序列的划分方法
- 将基准元素存入临时单元
p
i
v
o
t
pivot
pivot;
- 设置指针i指向待排序元素序列的首元素(不包括基准元素);
- 从左向右进行扫描,凡是比基准元素的排序码小的元素都交换到左边;
- 最后将基准元素放入最终位置,得到划分结果。
单指针实现
int Partition(vector<int>& nums, int low, int high) {
int tmp;
int i, k = low;
int pivot = nums[low];
for (i = low + 1; i <= high; i++) {
if (nums[i] < pivot) {
k++;
if (k != i) {
tmp = nums[i];
nums[i] = nums[k];
nums[k] = tmp;
}
}
}
nums[low] = nums[k];
nums[k] = pivot;
return k;
}
void QuickSort(vector<int>& nums, int left, int right) {
if (left < right) {
int pivotpos = Partition(nums, left, right);
QuickSort(nums, left, pivotpos - 1);
QuickSort(nums, pivotpos + 1, right);
}
}
使用双指针实现
- 将待排序序列第一个元素取出,作为基准元素,此时待排序序列第一个位置为空;
- 设置尾部指针
r
i
g
h
t
right
right往前遍历,找到一个排序码小于基准元素的元素,将该元素移动到待排序序列的第一个位置;
- 设置头部指针
l
e
f
t
left
left往后遍历,找到一个排序码大于基准元素的元素,将该元素移动到待排序序列中指针
r
i
g
h
t
right
right指向的位置;
- 重复2,3步骤,直到不满足
l
e
f
t
<
r
i
g
h
t
left<right
left<right,
l
e
f
t
left
left指向的位置即基准元素
p
i
v
o
t
pivot
pivot的最终位置。
void QuickSort2(vector<int>& nums, int low, int high) {
if (low < high) {
int left = low, right = high, pivot = nums[left];
while (left < right) {
while (left <right && nums[right]>pivot) {
right--;
}
if (left < right) {
nums[left] = nums[right];
left++;
}
while (left < right && nums[left] < pivot)
left++;
if (left < right) {
nums[right] = nums[left];
right--;
}
}
nums[left] = pivot;
QuickSort2(nums, low, left - 1);
QuickSort2(nums, left + 1, high);
}
}
时间复杂度
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n),空间复杂度介于
O
(
l
o
g
2
n
)
O(log_2n)
O(log2?n)和
O
(
1
)
O(1)
O(1)之间,不稳定排序
归并排序
将两个或两个以上的有序序列合并成一个新的有序序列。二路归并排序的算法思想如下:
- 设初始序列含有
n
n
n个元素,可看成
n
n
n个有序的子序列;
- 对
n
n
n个有序序列两两合并,得到
?
n
/
2
?
\lceil n/2 \rceil
?n/2?个长度为2或1的有序序列;
- 继续两两合并,直到得到一个长度为
n
n
n的有序序列为止。
排序小结
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用性 | 性质 |
---|
直接插入排序 |
O
(
n
2
)
O(n^2)
O(n2) |
O
(
1
)
O(1)
O(1) | 稳定 | 顺序、链式存储 | 比较次数和移动次数均取决于待排序表的初始状态 | 折半插入排序 |
O
(
n
2
)
O(n^2)
O(n2) |
O
(
1
)
O(1)
O(1) | 稳定 | 顺序存储 | 比较次数与初始状态无关,约O(nlogn),移动次数与初始状态相关 | 希尔排序 |
O
(
n
)
O(n)
O(n)~
O
(
n
2
)
O(n^2)
O(n2) |
O
(
1
)
O(1)
O(1) | 不稳定 | 顺序存储 | | 冒泡排序 |
O
(
n
2
)
O(n^2)
O(n2) |
O
(
1
)
O(1)
O(1) | 稳定 | 顺序、链式存储 | 有序子序列是全局有序的,一趟排序定位一个元素 | 快速排序 |
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n) |
O
(
l
o
g
2
n
)
O(log_2n)
O(log2?n)~
O
(
1
)
O(1)
O(1) | 不稳定 | 顺序存储 | 平均性能最优,运行时间与划分是否对称有关,一趟排序定位一个元素 | 简单选择排序 |
O
(
n
2
)
O(n^2)
O(n2) |
O
(
1
)
O(1)
O(1) | 不稳定 | 顺序存储 | 比较次数与初始状态无关,n(n-1)/2 | 堆排序 |
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n) |
O
(
1
)
O(1)
O(1) | 不稳定 | 顺序存储 | 一趟排序定位一个元素 | 归并排序 |
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n) |
O
(
n
)
O(n)
O(n) | 稳定 | | | 基数排序 |
O
(
d
(
n
+
r
)
)
O(d(n+r))
O(d(n+r)) |
O
(
r
)
O(r)
O(r) | 稳定 | | 时间复杂度与初始状态无关,d为趟数,r为基数,n为元素个数 |
|