Home

cuipf blog

10 Nov 2018

基础总结-数据结构

[toc]

排序

冒泡排序

原理:

两两比较相邻记录的关键字,如果反序则交换。时间复杂度最好情况下: o(n),最坏是(n*n)

//冒泡排序 相邻两个元素比较 时间复杂度:最坏是O(n^2)	最好的情况下O(n)
void  BubbleSort(int* a, int len)
{
	for (int i = 0; i < len; ++i)
	{
		bool noswap = true;
		for (int j = 0; j < len - i - 1; ++j)
		{
			if (a[j] > a[j + 1])
			{
				int temp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = temp;
				noswap = false;
			}
		}
		if (noswap)
		{
			break;
		}
	}
}

直接插入排序

原理:

直接插入排序算法 基本操作是将一个记录插入到已经排好序的有序表中,从而等到一个新的,元素加1的有序表 知道结束;

时间复杂度:最坏是O(n^2) 最好的情况下O(n)

void InsertSort(int* a, int len)
{
	int idx = 0;
	int temp = 0;
	for (int i = 1; i < len; ++i)
	{	
		temp = a[i];
		for (idx = i - 1; idx >= 0 && a[idx] > temp; --idx)
		{
			a[idx + 1] = a[idx];
		}
		a[idx + 1] = temp;
	}
}

选择排序

原理:

每次从乱序数组中找到最大(最小)的值,放在当前乱序数组头部,最终使数组有序; 时间复杂度:平均为O(n^2);最坏为:O(n^2)

void SelectSort(int* a, int len)
{
	for (int i = 0; i < len; ++i)
	{
		int min = i;
		for (int j = i + 1; j < len; ++j)
		{
			if (a[j] < a[min])
			{
				min = j;
			}
		}
		if (i != min)
		{
			int temp = a[i];
			a[i] = a[min];
			a[min] = temp;
		}
	}
}

快速排序

选择一个基准数,把比这个数小的挪到左边 这个数大的移动到右边 然后不断对左右两边执行这个操作 直到数组有序; 时间复杂度:平均时间复杂度:O(nlog2n); 最坏是:O(n^2);

//快速排序 	选择一个基准数,把比这个数小的挪到左边 这个数大的移动到右边 然后不断对左右两边执行这个操作 直到数组有序
//时间复杂度:平均时间复杂度:O(nlog2n);
int Partition(int* nums, int low, int high);
void QuickSort(int* nums, int begin, int end)
{
	if (begin < end)
	{
		int key = Partition(nums, begin, end);
		if (key - 1 > 0)
		{
			QuickSort(nums, begin, key - 1);
		}
		if (key + 1 < end)
		{
			QuickSort(nums, key + 1, end);
		}
	}
}
/*
*	第一趟快速排序
*/
int Partition(int* nums, int low, int high)
{
	if (nums == nullptr || low >= high)
    {
        //input invaild
        return -1;
    }

    int guard = nums[low];
    while (low < high)
    {
        while (low < high && nums[high] >= guard)
        {
            --high;
        }
        if (low < high)
        {
            nums[low] = nums[high];
            ++low;
        }
        while (low < high && nums[low] < guard)
        {
            ++low;
        }
        if (low < high)
        {
            nums[high] = nums[low];
            --high;
        }
    }
	nums[low] = guard;
	return low;
}

希尔(shell)排序

原理: 希尔排序:又叫缩小增量排序方法;是一种基于插入思想的排序方法; 通过逐一减少增量的方法,使得一个数组逐渐达到有序的目的;以上就是希尔排序的思路了,总的来说,就是每次选定一个步长,将数组进行划分,然后对每列小划分利用直接插入排序方法排序,得到新数组,再选择步长分组,利用直接插入排序。直到步长为1,最后利用一次直接插入排序。说白了,希尔排序就是选择合适步长+直接插入排序。

时间复杂度:平均是O(n^1.3),最好:O(n) 最坏:O(n^2)

//shell排序  希尔排序是将数组按照一定步长分成几个子数组进行排序,通过逐渐缩短步长来完成最终排序
//时间复杂度:O(nlogn) ~~ O(n^2);
void ShellSort(int* a, int len)
{
	for (int gap = len >> 1; gap > 0; gap >>= 1)
	{
		for (int i = gap; i < len; ++i)
		{
			int temp = a[i];
			int j = i - gap;
			for (; j >= 0 && a[j] > temp; j -= gap)
			{
				a[j + gap] = a[j];
			}
			a[j + gap] = temp;
		}
	}
}

归并排序

归并排序的基本思想是将若干个序列进行两两归并,直至所有待排序记录都在一个有序序列为止;

时间复杂度: 平均时间复杂度: O(nlog2n); 最好最坏都是: O(nlog2n);

//归并排序 将一个数组打散成小数组,然后把小数组拼凑再排序知道数组有序
void MergeArray(int* nums, int begin, int mid, int end, int* temp)
{
	int low = begin;
	int high = mid; 
	int temp_idx = begin;
	while (low != mid && high != end)
	{
		if (nums[low] < nums[high])
		{
			temp[temp_idx++] = nums[low++];
		}
		else
		{
			temp[temp_idx++] = nums[high++];
		}
	}
	while (low < mid)
	{
		temp[temp_idx++] = nums[low++];
	}
	while (high < end)
	{
		temp[temp_idx++] = nums[high++];
	}
}

void MergeSort(int* nums, int begin, int end, int* temp)
{
	int mid = (begin + end) / 2;
	if (mid != begin) 
	{
		MergeSort(nums, begin, mid, temp);
		MergeSort(nums, mid, end, temp);
		MergeArray(nums, begin, mid, end, temp);
	}
}

排序算法时间复杂度

image image

堆(Heap)分为小根堆和大根堆两种;

对于一个小根堆,它是具有如下特性的一棵完全二叉树:

  • 若树根结点存在左孩子,则根结点的值(或某个域的值)小于等于左孩子结点的值(或某个域的值);
  • 若树根结点存在右孩子,则根结点的值(或某个域的值)小于等于右孩子结点的值(或某个域的值);
  • 以左、右孩子为根的子树又各是一个堆。

大根堆的定义与上述类似,只要把小于等于改为大于等于就得到了。

重建堆

当堆顶改变时候,如何重建堆??

重建堆

/*
* 调整堆
* 参数:heap[begin, end]是以heap[begin]为根的完全二叉树;
* 且分别以heap[2*begin] 和 [2*begin + 1]为根的左右子树为大根堆;
* 调整heap[begin]使其符合大根堆;
*/
void AdjustHeap(int* heap, int begin, int end)
{
	if (heap == nullptr || begin >= end)
	{
		return;
	}
	int root_value = heap[begin];
	int root = begin;
	int child = root * 2;
	bool finish = false;
	while (child <= end && !finish)
	{
		if (child < end && heap[child] < heap[child + 1])
		{
			++child;
		}
		if (root_value >= heap[child])
		{
			finish = true;
		}
		else
		{
			heap[root] = heap[child];
			root = child;
			child = root * 2;
		}
	}
	heap[root] = root_value;
}

如何由一任意序列出建堆

建初堆

void CreateHeap(int* heap, int length)
{
	if (heap == nullptr || length == 0)
	{
		return;
	}
	//从length / 2个记录开始进行筛选建堆
	for (int index = length / 2; index >= 0; --index)
	{
		AdjustHeap(heap, index, length - 1);
	}
}

堆排序

堆排序

/*
*	堆排序 时间复杂度: 平均最好最坏都是O(log2n)
*/
void SortHeap(int* heap, int length)
{
	int mid = length / 2;
	for (int index = length - 1; index >= 1; --index)
	{
		//根节点和尾节点交换
		int temp = heap[0];
		heap[0] = heap[index];
		heap[index] = temp;
		AdjustHeap(heap, 0, index - 1);
	}
}

查找算法

顺序查找

顺序查找算法 无序查找,属于线性查找 从序列到一端开始一次遍历; 时间复杂度:O(n)

int SequenceSearch(int arr[], int length, int value)
{
	int index = 0;
	for (int idx = 0; idx < length; ++idx)
	{
		if (arr[idx] == value)
		{
			return idx;
		}
	}
	return -1;
}

二分查找

二分查找也叫折半查找;

  • 思想:根据给定的值和序列的值对比,相等 找到;不等 再依次查找左右子序列;
  • 注意:1. 二分查找的序列必须为有序序列,如果是无序的话,先排序; 2. 对于插入删除频繁的队列,维护有序队列成本较高,不建议使用;
  • 时间复杂度:最坏是:O(log2(n + 1)); 平均:O(log2n)
int BinarySearch(int arr[], int length, int value)
{
	int mid = 0;
	int low = 0;
	int high = length - 1;
	while (low < high)
	{
		mid = (low + high) / 2;
		if (arr[mid] == value)
		{
			return mid;
		}
		else if (arr[mid] < value)
		{
			low = mid + 1;
		}
		else
		{
			high = mid - 1;
		}
	}
	return -1;
}

//迭代查找
int BinarySearchRe(int arr[], int low, int high, int value)
{
	if (low < high)
	{
		return -1;
	}

	int mid = (low + high) / 2;
	if (arr[mid] == value)
	{
		return mid;
	}
	else if (arr[mid] < value)
	{
		return BinarySearchRe(arr, mid + 1, high, value);
	}
	else
	{
		return BinarySearchRe(arr,  low,  mid - 1, value);
	}
}

插值查找算法

  • 思想:基于二分查找算法,根据查找点在序列中的大概位置,将查找点修改为自适应的,提高查找效率;
  • 注: 对于表长较大,而且元素分布比较均匀的表来说,该算法平均性能比二分查找要好很多,反之,不一定合适
  • 时间复杂度:查找成功或者失败的时间复杂度均为O(log2(log2n))
  • int InsertionSearch(int arr[], int length, int value)
    {
    int mid = 0;
    int low = 0;
    int high = length - 1;
    while (low < high)
    {
        mid = low + (value - arr[low]) / (arr[high] - arr[low])*(high - low);
        if (arr[mid] == value)
        {
            return mid;
        }
        else if (arr[mid] < value)
        {
            low = mid + 1;
        }
        else
        {
            high = mid - 1;
        }
    }
    return -1;
    }
    

树的类型

二叉排序树

二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:

  • 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 任意节点的左、右子树也分别为二叉查找树。
  • 二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。

二叉排序树创建,插入,查找

二叉排序树的查找效率:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。查找长度: n(当为单支树), 平均长度:n(n+1)/2;

/*
 * 关于二叉排序树的插入、生成、查找
 */

/*
 * 根据二叉排序树的性质,二叉排序树插入
 */
typedef BinaryTreeNode BSTree;
void InsertBST(BSTree** tree, int key)
{
    //二叉排序树插入遇到tree的value和key相等 直接退出
    BSTree* bstree = nullptr;
    if (*tree == nullptr)
    {
        bstree = new BinaryTreeNode();
        bstree->value = key;
        bstree->left = nullptr;
        bstree->right = nullptr;
        *tree = bstree;
    }
    else if ((*tree)->value < key)
    {
        InsertBST(&((*tree)->right), key);
    }
    else if ((*tree)->value > key)
    {
        InsertBST(&((*tree)->left), key);
    }
}

/*
 * 创建二叉排序树
 */
void CreateBST(int* tree, int length)
{
    BSTree* bt = nullptr;
    BSTree** bstree = &bt;
    for (int index = 0; index < length; ++index)
    {
        InsertBST(bstree, tree[index]);
    }
}

/*
 * 二叉排序树 查找 递归
 */

BSTree* SearchBSTRe(BSTree* tree, int key)
{
    if (tree == nullptr)
    {
       return nullptr;
    }
    else if (tree->value < key)
    {
        return SearchBSTRe(tree->right, key);
    }
    else
    {
        return SearchBSTRe(tree->left, key);
    }
}

/*
 * Binary Sort Tree search  not recursion
 */
BSTree* SearchBST(BSTree* tree, int key)
{
    BSTree* temp = tree;
    while (temp)
    {
        if (temp->value == key)
        {
            return temp;
        }
        else if (temp->value < key)
        {
            temp = temp->right;
        }
        else
        {
            temp = temp->left;
        }
    }
}

平衡二叉排序树

平衡二叉排序树又称为AV树;一课平衡二叉树或者是空树,或者具有以下性质:

  • 左子树和右子树的深度差绝对值小于等于1;
  • 左右子树也是平衡二叉排序树;

平衡二叉排序树引入的目的:为了提高查找效率,其平均查找长度为O(log2n);

hash

哈希法又称散列法,我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素”分类”,然后将这个元素存储在相应”类”所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了”冲突”;

什么是哈希函数?

哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。

哈希函数本身便于计算;计算出来的结构分布均匀,尽可能的减少冲突;

哈希函数构造方式

数据分析法;

预先知道所有的数据,分析数据非分布,找出相应的方式;

平方取中法;

分段叠加法;

除留余数法;

伪随机法;

处理冲突的方法

开放地址法;

再哈希法;

链地址法;

建立公共溢出区;

hash总结

哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。    复杂度分析: 单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。

使用Hash,我们付出了什么?

我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KVpair; python 中的map本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么?

Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。

cuipf

scribble