刘宇波《算法与数据结构》 学习笔记
为什么要学O(n^2)时间复杂度的算法?
- 基础, 可由简单衍生出复杂的算法
- 简单场景下易于实现
- 特殊情况下,可能更优
- 作为的子过程,对复杂排序算法优化
准备工作
为了方便排序算法的测试,先创建一个随机数产生方法和一个打印方法。
新建一个头文件 SortTestHelper.h
//
// Created by BMElab on 9/3/2019.
//
#ifndef SORT_SORTTESTHELPER_H
#define SORT_SORTTESTHELPER_H
#include <iostream>
#include <ctime>
#include <cassert>
using namespace std;
namespace SortTestHelper{
int* genRandomArray(int n, int rangeL, int rangeR){
assert(rangeL < rangeR);
int *arr = new int[n];
srand(time(NULL));
for (int i=0; i<n; i++)
arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
return arr;
}
void printArray(int n, int *arr){
for (int i=0; i<n; i++)
std::cout << arr[i] << std::ends;
}
}
#endif //SORT_SORTTESTHELPER_H
测试两个方法:
#include <iostream>
#include "SortTestHelper.h"
int main() {
int n = 10;
int *arr = SortTestHelper::genRandomArray(n, 0, n);
int setArr[10] = {1, 2, 3, 4, 5, 6, 6, 7, 8, 9};
SortTestHelper::printArray(n, arr);
SortTestHelper::printArray(n, setArr);
}
运行结果:
D:\cpp_projects\sort\cmake-build-debug\sort.exe
5 2 6 1 10 4 10 1 2 1 1 2 3 4 5 6 6 7 8 9
Process finished with exit code 0
选择排序
void sectionSort(int n, int arr[]){
for (int i=0; i<n; i++){
int minInd = i;
for (int j=i+1; j<n; j++){
if (arr[j] < arr[minInd])
minInd = j;
}
swap(arr[i], arr[minInd]);
}
}
这里的swap是标准库中的函数。
测试:
#include <iostream>
#include "SortTestHelper.h"
using namespace std;
void sectionSort(int arr[], int n){
for (int i=0; i<n; i++){
int minInd = i;
for (int j=i+1; j<n; j++){
if (arr[j] < arr[minInd])
minInd = j;
}
swap(arr[i], arr[minInd]);
}
}
int main() {
int n = 10;
int setArr[10] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
sectionSort(setArr, n);
SortTestHelper::printArray(setArr, n);
return 0;
}
关于 using namespace std 的使用问题,会对命名空间造成污染,这里不考虑那么多,只为了快速方便实现算法而使用。
模板函数
使用模板使排序算法能够对多种数据类型进行排序
将排序算法声明为模板函数并修改其参数如下:
template <typename T>
void sectionSort(T arr[], int n){
for (int i=0; i<n; i++){
int minInd = i;
for (int j=i+1; j<n; j++){
if (arr[j] < arr[minInd])
minInd = j;
}
swap(arr[i], arr[minInd]);
}
}
创建一个模板,并将形参类型改为T即可。
测试:
#include <iostream>
#include "SortTestHelper.h"
#include <string>
using namespace std;
template <typename T>
void sectionSort(T arr[], int n){
for (int i=0; i<n; i++){
int minInd = i;
for (int j=i+1; j<n; j++){
if (arr[j] < arr[minInd])
minInd = j;
}
swap(arr[i], arr[minInd]);
}
}
int main() {
int n = 10;
int setArrA[10] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
sectionSort(setArrA, n);
for (int i=0; i<n; i++)
cout<<setArrA[i]<<ends;
cout << endl;
float setArrB[5] = {10.1, 9.1, 8.1, 7.1, 6.1};
sectionSort(setArrB, 5);
for (int i=0; i<5; i++)
cout<<setArrB[i]<<ends;
cout << endl;
string setArrC[5] = {"E", "D", "C", "B", "A"};
sectionSort(setArrC, 5);
for (int i=0; i<5; i++)
cout<<setArrC[i]<<ends;
cout << endl;
return 0;
}
结果:
D:\cpp_projects\sort\cmake-build-debug\sort.exe
1 2 3 4 5 6 7 8 9 10
6.1 7.1 8.1 9.1 10.1
A B C D E
建立测试模板
使用测试模板更为简便地测试每个算法的效率和正确性:
修改前面的SortTestHelper.h头文件,添加两个函数isTrue(), testSort().
isTrue()为了确定排序是否正确。
testSort(string sortName, void(*sort)(int, T[]), T arr[], int n)
用于测试排序算法的效率,第一个参数是要测试的函数名,第二个参数为一个指针,指向被测试函数,第三个参数为测试的数组,第四个参数为被测数据个数。
//
// Created by BMElab on 9/3/2019.
//
#ifndef SORT_SORTTESTHELPER_H
#define SORT_SORTTESTHELPER_H
#include <iostream>
#include <ctime>
#include <cassert>
using namespace std;
namespace SortTestHelper{
int* genRandomArray(int n, int rangeL, int rangeR){
assert(rangeL < rangeR);
int *arr = new int[n];
srand(time(NULL));
for (int i=0; i<n; i++)
arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
return arr;
}
template <typename T>
void printArray(T arr[], int n){
for (int i=0; i<n; i++)
std::cout << arr[i] << std::ends;
cout<<endl;
return;
}
template <typename T>
bool isTrue(T arr[], int n){
for (int i=0; i<n-1; i++)
if(arr[i] > arr[i+1])
return false;
return true;
}
// build a test sort function for all kinds of sort methods
template <typename T>
void testSort(string sortName, void(*sort)(T[], int), T arr[], int n){
clock_t startTime = clock();
sort(arr, n);
clock_t endTime = clock();
assert(isTrue(arr, n));
cout << sortName<<" " << double(endTime - startTime) / CLOCKS_PER_SEC << " s" << endl;
return;
}
}
#endif //SORT_SORTTESTHELPER_H
测试:
#include <iostream>
#include "SortTestHelper.h"
using namespace std;
template <typename T>
void sectionSort(T arr[], int n){
for (int i=0; i<n; i++){
int minInd = i;
for (int j=i+1; j<n; j++){
if (arr[j] < arr[minInd])
minInd = j;
}
swap(arr[i], arr[minInd]);
}
}
int main(){
int n = 100000;
int *arr = SortTestHelper::genRandomArray(n, 0, n);
sectionSort(arr, n);
SortTestHelper::testSort("sectionSort", sectionSort, arr, n);
delete[] arr;
return 0;
}
结果:
sectionSort 12.007 s
插入排序
遍历数组,依次比较当前位置 j 上的值与其j-1的值大小, 如果当前值小于前一值,则交换两个数的位置,继续比较 j-1与 j-2 位置上的值,直到 j-n=0。
template <typename T>
void insertSort(T arr[], int n){
for (int i=1; i<n; i++){
for (int j=i; j>0 && arr[j-1] > arr[j]; j--)
swap(arr[j-1], arr[j]);
}
}
性能比较和分析
#include <iostream>
#include "SortTestHelper.h"
//#include <string>
using namespace std;
template <typename T>
void sectionSort(T arr[], int n){
for (int i=0; i<n; i++){
int minInd = i;
for (int j=i+1; j<n; j++){
if (arr[j] < arr[minInd])
minInd = j;
}
swap(arr[i], arr[minInd]);
}
}
template <typename T>
void insertSort(T arr[], int n){
for (int i=1; i<n; i++){
for (int j=i; j>0 && arr[j-1] > arr[j]; j--)
swap(arr[j-1], arr[j]);
}
}
int main(){
int n = 10000;
int *arr = SortTestHelper::genRandomArray(n, 0, n);
int *copyArr = SortTestHelper::copyArray(arr, n);
SortTestHelper::testSort("sectionSort", sectionSort, arr, n);
SortTestHelper::testSort("insertSort", insertSort, copyArr, n);
delete[] arr;
delete[] copyArr;
return 0;
}
D:\cpp_projects\sort\cmake-build-debug\sort.exe
sectionSort 0.135 s
insertSort 0.351 s
上面的SortTestHelper::copyArray(arr, n)函数定义在头文件SortTestHelper中
template <typename T>
T* copyArray(T arr[], int n){
T* new_arr = new int[n];
copy(arr, arr+n, new_arr);
return new_arr;
}
对于万级的数据量,插入排序的性能比选择排序差。
理论上插入排序应该不选择排序性能好,因为选择排序第二轮循环中每次都要对剩下的所有数进行比较,而插入排序在不满足arr[j-1] > arr[j]时能够提前停止。
为什么还插入排序的性能比选择排序差?
分析具体的实现过程,我们发现在插入排序中,第二轮循环会发生多次的交换操作。 虽然比较的次数相对于选择排序少了,但是交换次数远远多于选择排序。
交换操作设计到数组的索引,3次复制,耗时远大于比较操作。
所以改进的方法就是减少交换操作的次数,使用赋值操作代替交换操作。
插入算法的改进
template <typename T>
void insertSort1(T arr[], int n){
for (int i=1; i<n; i++){
T cur = arr[i];
int j;
for (j=i; j>0 && arr[j - 1]>cur; j--)
arr[j] = arr[j - 1];
arr[j] = cur;
}
}
先复制当前的值T cur = arr[i], 当在第二轮循环中arr[j-1] > cur ,将前一个值后移arr[j] = arr[j - 1]。进行下轮比较,每次比较都是与复制的cur比较,若不满足arr[j - 1] > cur,说明当前的值cur处在正确的位置上,arr[j] = cur。
测试结果:
D:\cpp_projects\sort\cmake-build-debug\sort.exe
sectionSort 0.133 s
insertSort 0.303 s
insertSort1 0.122 s
插入排序优于选择排序。
插入排序的重要特性
第二轮循环提前终止是插入排序的重要特性。
在近乎有序的数组中,插入排序性能远远优于选择排序。
在小规模的数列中,数据就近乎有序,因此在复杂的排序中,常使用插入排序作为最后排序的优化算法。