阅读目录
1. 顺序查找
2. 二分查找
3. 插值查找
4. 斐波那契查找
5. 分块查找
6. 树表查找
7. 哈希查找
下面所有的代码,都已经经过vs测试。
1.顺序查找
基本思想:顺序查找也称为线形查找,属于无序查找算法。
C#算法实现:
/// <summary>
/// 顺序查找
/// </summary>
private static int SequenceSearch(int[] arr, int value)
{
int length = arr.Length;
for (int i = 0; i < length; i++)
{
if(arr[i] == value)
{
return i;
}
}
return -1;
}
2. 二分查找
基本思想:元素必须是有序的,属于有序查找算法。
注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》
C#算法实现:
/// <summary>
/// 二分法查找,非递归方法实现,二分查找的条件是原数组有序
/// 没有找到,返回-1;找到了,则返回索引
/// </summary>
/// <param name="arr"></param>
/// <param name="low"></param>
/// <param name="height"></param>
/// <param name="value"></param>
private static int BinarySearch(int[] arr, int low, int height, int value)
{
if (arr == null || arr.Length == 0 || low >= height)
{
return -1;
}
int hi = height - 1;
int lowValue = arr[low];
int heightValue = arr[hi];
if (lowValue > value || value > heightValue)
{
return -1;
}
int mid;
while (low <= hi)
{
mid = (low + hi) >> 1;
int item = arr[mid];
if (item == value)
{
return mid;
}
else if (item > value)
{
hi = mid - 1;
}
else
{
low = mid + 1;
}
}
return -1;
}
/// <summary>
/// 二分法查找,递归方法实现,二分查找的条件是原数组有序
/// 没有找到,返回-1;找到了,则返回索引
/// </summary>
/// <param name="arr"></param>
/// <param name="low"></param>
/// <param name="height"></param>
/// <param name="value"></param>
/// <returns></returns>
private static int BinarySearchRecursive(int[] arr, int low, int height, int value)
{
if (arr == null || arr.Length == 0 || low >= height)
{
return -1;
}
int hi = height - 1;
int lowValue = arr[low];
int heightValue = arr[hi];
if (lowValue > value || value > heightValue)
{
return -1;
}
int mid;
if (low <= hi)
{
mid = (low + hi) >> 1;
int item = arr[mid];
if (item == value)
{
return mid;
}
else if (item > value)
{
return BinarySearchRecursive(arr, low, mid, value);
}
else
{
return BinarySearchRecursive(arr, mid + 1, hi, value);
}
}
return -1;
}
其中位运算,mid = (low + hi) >> 1;相当于mid = (low + hi) / 2;
相比于除法运算,位运算更快。
3.插值查找
基本思想:插值查找,是对二分法查找的优化,插值优化了mid,使之更接近查找数值在有序序列的实际位置,也是有序查找。
C#算法实现:
/// <summary>
/// 插值查找,是对二分法查找的优化
/// 二分法: mid = low + 1/2 *(high - low)
/// 插值查找:mid = low + ((point-array[low])/(array[high]-array[low]))*(high-low)
/// 插值优化了mid,使之更接近查找数值在有序序列的实际位置
/// </summary>
/// <param name="arr"></param>
/// <param name="low"></param>
/// <param name="height"></param>
/// <param name="value"></param>
/// <returns></returns>
private static int InterpolationSearch(int[] arr, int low, int height, int value)
{
if (arr == null || arr.Length == 0 || low >= height)
{
return -1;
}
int hi = height - 1;
int lowValue = arr[low];
int heightValue = arr[hi];
if (lowValue > value || value > heightValue)
{
return -1;
}
int mid;
while (low <= hi)
{
mid = low + ((value - lowValue) / (heightValue - lowValue)) * (hi - low);
int item = arr[mid];
if (item == value)
{
return mid;
}
else if (item > value)
{
hi = mid - 1;
}
else
{
low = mid + 1;
}
}
return -1;
}
4.斐波那契查找
基本思想:在二分查找的基础上根据斐波那契数列进行分割的。
C#算法实现:
/// <summary>
/// 斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。
/// 在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],
/// 将原查找表扩展为长度为F[n](如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素),
/// 完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,
/// 那么前半段元素个数和整个有序表长度的比值就接近黄金比值0.618,
/// 找出要查找的元素在那一部分并递归,直到找到。
/// middle = low + fb[k - 1] - 1
/// </summary>
private static int FbSearch(int[] arr, int value)
{
if (arr == null || arr.Length == 0)
{
return -1;
}
int length = arr.Length;
// 创建一个长度为20的斐波数列
int[] fb = MakeFbArray(20);
int k = 0;
while (length > fb[k] - 1)
{
// 找出数组的长度在斐波数列(减1)中的位置,将决定如何拆分
k++;
}
// 满足黄金比例分割
if (length == fb[k - 1])
{
return FindFbSearch(arr, fb, --k, value, length);
}
else
{
// 构造一个长度为fb[k] - 1的新数列
int[] temp = new int[fb[k] - 1];
// 把原数组拷贝到新的数组中
arr.CopyTo(temp, 0);
int tempLen = temp.Length;
for (int i = length; i < tempLen; i++)
{
// 从原数组长度的索引开始,用最大的值补齐新数列
temp[i] = arr[length - 1];
}
return FindFbSearch(temp, fb, k, value, length);
}
}
private static int FindFbSearch(int[] arr, int[] fb, int k, int value, int length)
{
int low = 0;
int hight = length - 1;
while (low <= hight)
{
// 黄金比例分割点
int middle = low + fb[k - 1] - 1;
if (arr[middle] > value)
{
hight = middle - 1;
// 全部元素 = 前半部分 + 后半部分
// 根据斐波那契数列进行分割,F(n)=F(n-1)+F(n-2)
// 因为前半部分有F(n-1)个元素,F(n-1)=F(n-2)+F(n-3),
// 为了得到前半部分的黄金分割点n-2,
// int middle = low + fb[k - 1] - 1; k已经减1了
// 所以k = k - 1
k = k - 1;
}
else if (arr[middle] < value)
{
low = middle + 1;
// 全部元素 = 前半部分 + 后半部分
// 根据斐波那契数列进行分割,F(n)=F(n-1)+F(n-2)
// 因为后半部分有F(n-2)个元素,F(n-2)=F(n-3)+F(n-4),
// 为了得到后半部分的黄金分割点n-3,
// int middle = low + fb[k - 1] - 1; k已经减1了
// 所以k = k - 2
k = k - 2;
}
else
{
if (middle <= hight)
{
return middle;// 若相等则说明mid即为查找到的位置
}
else
{
return hight;// middle的值已经大于hight,进入扩展数组的填充部分,即原数组最后一个数就是要查找的数
}
}
}
return -1;
}
public static int[] MakeFbArray(int length)
{
int[] array = null;
if (length > 2)
{
array = new int[length];
array[0] = 1;
array[1] = 1;
for (int i = 2; i < length; i++)
{
array[i] = array[i - 1] + array[i - 2];
}
}
return array;
}
5.分块查找
基本思想:分块查找要求把一个数据分为若干块,每一块里面的元素可以是无序的,但是块与块之间的元素需要是有序的。
C#算法实现:
public struct IndexBlock
{
public int max;
public int start;
public int end;
};
const int BLOCK_COUNT = 3;
private static void InitBlockSearch()
{
int j = -1;
int k = 0;
int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
IndexBlock[] indexBlock = new IndexBlock[BLOCK_COUNT];
for (int i = 0; i < BLOCK_COUNT; i++)
{
indexBlock[i].start = j + 1; //确定每个块范围的起始值
j = j + 1;
indexBlock[i].end = j + 4; //确定每个块范围的结束值
j = j + 4;
indexBlock[i].max = a[j]; //确定每个块范围中元素的最大值
}
k = BlockSearch(12, a, indexBlock);
if (k >= 0)
{
Console.WriteLine("查找成功!你要查找的数在数组中的索引是:{0}\n", k);
}
else
{
Console.WriteLine("查找失败!你要查找的数不在数组中。\n");
}
}
/// <summary>
/// 分块查找
/// 分块查找要求把一个数据分为若干块,每一块里面的元素可以是无序的,但是块与块之间的元素需要是有序的。
/// (对于一个非递减的数列来说,第i块中的每个元素一定比第i-1块中的任意元素大)
/// </summary>
private static int BlockSearch(int x, int[] a, IndexBlock[] indexBlock)
{
int i = 0;
int j;
while (i < BLOCK_COUNT && x > indexBlock[i].max)
{
//确定在哪个块中
i++;
}
if (i >= BLOCK_COUNT)
{
//大于分的块数,则返回-1,找不到该数
return -1;
}
//j等于块范围的起始值
j = indexBlock[i].start;
while (j <= indexBlock[i].end && a[j] != x)
{
//在确定的块内进行查找
j++;
}
if (j > indexBlock[i].end)
{
//如果大于块范围的结束值,则说明没有要查找的数,j置为-1
j = -1;
}
return j;
}
附上:上面的五种查找的测试用例:
static void Main(string[] args)
{
//已经排序好的int[]
int[] arr = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int length = arr.Length;
//int index = SequenceSearch(arr, 4);
//InitBlockSearch();
int index = BinarySearch(arr, 0, length, 4);
//int index = InterpolationSearch(arr, 0, length, 4);
//int index = FbSearch(arr, 4);
if (index >= 0 && index < length)
{
Console.WriteLine("已找到,索引位置为 " + index);
}
else
{
Console.WriteLine("未找到");
}
Console.ReadLine();
}
6.树表查找
6.1最简单的树表查找算法——二叉树查找算法
基本思想:二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。
二叉树的性质:
1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3)任意节点的左、右子树也分别为二叉查找树。
C#算法实现:
using System;
using System.Collections.Generic;
namespace StructScript
{
public class BinaryTree<T>
{
//根节点
private TreeNode<T> mRoot;
//比较器
private Comparer<T> mComparer;
public BinaryTree()
{
mRoot = null;
mComparer = Comparer<T>.Default;
}
public bool Contains(T value)
{
if (value == null)
{
throw new ArgumentNullException();
}
TreeNode<T> node = mRoot;
while (node != null)
{
int comparer = mComparer.Compare(value, node.Data);
if (comparer > 0)
{
node = node.RightChild;
}
else if (comparer < 0)
{
node = node.LeftChild;
}
else
{
return true;
}
}
return false;
}
public void Add(T value)
{
mRoot = Insert(mRoot, value);
}
private TreeNode<T> Insert(TreeNode<T> node, T value)
{
if (node == null)
{
return new TreeNode<T>(value, 1);
}
int comparer = mComparer.Compare(value, node.Data);
if (comparer > 0)
{
node.RightChild = Insert(node.RightChild, value);
}
else if (comparer < 0)
{
node.LeftChild = Insert(node.LeftChild, value);
}
else
{
node.Data = value;
}
return node;
}
public int Count
{
get
{
return CountLeafNode(mRoot);
}
}
private int CountLeafNode(TreeNode<T> root)
{
if (root == null)
{
return 0;
}
else
{
return CountLeafNode(root.LeftChild) + CountLeafNode(root.RightChild) + 1;
}
}
public int Depth
{
get
{
return GetHeight(mRoot);
}
}
private int GetHeight(TreeNode<T> root)
{
if (root == null)
{
return 0;
}
int leftHight = GetHeight(root.LeftChild);
int rightHight = GetHeight(root.RightChild);
return leftHight > rightHight ? leftHight + 1 : rightHight + 1;
}
public T Max
{
get
{
TreeNode<T> node = mRoot;
while (node.RightChild != null)
{
node = node.RightChild;
}
return node.Data;
}
}
public T Min
{
get
{
if (mRoot != null)
{
TreeNode<T> node = GetMinNode(mRoot);
return node.Data;
}
else
{
return default(T);
}
}
}
public void DelMin()
{
mRoot = DelMin(mRoot);
}
private TreeNode<T> DelMin(TreeNode<T> node)
{
if (node.LeftChild == null)
{
return node.RightChild;
}
node.LeftChild = DelMin(node.LeftChild);
return node;
}
public void Remove(T value)
{
mRoot = Delete(mRoot, value);
}
private TreeNode<T> Delete(TreeNode<T> node, T value)
{
if (node == null)
{
throw new ArgumentNullException();
}
int comparer = mComparer.Compare(value, node.Data);
if (comparer > 0)
{
node.RightChild = Delete(node.RightChild, value);
}
else if (comparer < 0)
{
node.LeftChild = Delete(node.LeftChild, value);
}
else
{
// 1.如果删除节点没有子节点,直接返回null
// 2.如果只有一个子节点,返回其子节点代替删除节点即可
if (node.LeftChild == null)
{
return node.RightChild;
}
else if (node.RightChild == null)
{
return node.LeftChild;
}
else
{
// 3.当左右子节点都不为空时
// 找到其右子树中的最小节点,替换删除节点的位置
TreeNode<T> tempNode = node;
node = GetMinNode(tempNode.RightChild);
node.RightChild = DelMin(tempNode.RightChild);
node.LeftChild = tempNode.LeftChild;
}
}
return node;
}
private TreeNode<T> GetMinNode(TreeNode<T> node)
{
while (node.LeftChild != null)
{
node = node.LeftChild;
}
return node;
}
// 中序遍历:首先遍历其左子树,然后访问根结点,最后遍历其右子树。
// 递归方法实现体内再次调用方法本身的本质是多个方法的简写,递归一定要有出口
public void ShowTree()
{
ShowTree(mRoot);
}
private void ShowTree(TreeNode<T> node)
{
if (node == null)
{
return;
}
ShowTree(node.LeftChild);
//打印节点数据
Console.WriteLine(node.Data);
ShowTree(node.RightChild);
}
}
public class TreeNode<T>
{
//数据
public T Data { get; set; }
//左孩子
public TreeNode<T> LeftChild { get; set; }
//右孩子
public TreeNode<T> RightChild { get; set; }
public TreeNode(T value, int count)
{
Data = value;
LeftChild = null;
RightChild = null;
}
}
}
二叉树的查找:
将要查找的value和节点的value比较,如果小于,那么就在Left Node节点查找;如果大于,则在Right Node节点查找,如果相等,更新Value。
二叉树的删除:
1.如果删除节点没有子节点,直接返回null
2.如果只有一个子节点,返回其子节点代替删除节点即可
3.当左右子节点都不为空时,找到其右子树中的最小节点,替换删除节点的位置
6.2树表查找算法——红黑树
前面介绍了二叉查找树,他对于大多数情况下的查找和插入在效率上来说是没有问题的,但是他在最差的情况下效率比较低。
红黑树保证在最坏的情况下插入和查找效率都能保证在对数的时间复杂度内完成。
红黑树的性质:
性质1.节点是红色或黑色
性质2.根是黑色
性质3.所有叶子都是黑色(叶子是NIL节点)
性质4.如果一个节点是红的,则它的两个子节点都是黑的(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5.从任一节点到其叶子的所有路径都包含相同数目的黑色节点。
注:清晰理解红黑树的性质,对红黑树算法的实现有非常重要的作用。
最近发现一个旧金山大学计算机系的一个网站,可以根据你输入的插入、删除和查找的的值,提供非常直观的插入、删除和查找动画展示,删除选取的左子树的最大节点作为删除节点的替换节点。超链接地址
C#算法实现:
using System;
using System.Collections.Generic;
namespace StructScript
{
/// 红黑树定义:
/// 性质1.节点是红色或黑色
/// 性质2.根是黑色
/// 性质3.所有叶子都是黑色(叶子是NIL节点)
/// 性质4.如果一个节点是红的,则它的两个子节点都是黑的(从每个叶子到根的所有路径上不能有两个连续的红色节点)
/// 性质5.从任一节点到其叶子的所有路径都包含相同数目的黑色节点。
public class RedBlackTree<T>
{
//根节点
private RedBlackTreeNode<T> mRoot;
//比较器
private Comparer<T> mComparer;
private const bool RED = true;
private const bool BLACK = false;
public RedBlackTree()
{
mRoot = null;
mComparer = Comparer<T>.Default;
}
public bool Contains(T value)
{
RedBlackTreeNode<T> node;
return Contain(value, out node);
}
public bool Contain(T value, out RedBlackTreeNode<T> newNode)
{
if (value == null)
{
throw new ArgumentNullException();
}
newNode = null;
RedBlackTreeNode<T> node = mRoot;
while (node != null)
{
int comparer = mComparer.Compare(value, node.Data);
if (comparer > 0)
{
node = node.RightChild;
}
else if (comparer < 0)
{
node = node.LeftChild;
}
else
{
newNode = node;
return true;
}
}
return false;
}
public void Add(T value)
{
if (mRoot == null)
{
// 根节点是黑色的
mRoot = new RedBlackTreeNode<T>(value, BLACK);
}
else
{
// 新插入节点是红色的
Insert1(new RedBlackTreeNode<T>(value, RED), value);
}
}
private void Insert1(RedBlackTreeNode<T> newNode, T value)
{
//遍历找到插入位置
RedBlackTreeNode<T> node = mRoot;
//插入节点的父节点
RedBlackTreeNode<T> parent = null;
while (node != null)
{
parent = node;
int comparer = mComparer.Compare(value, node.Data);
if (comparer > 0)
{
node = node.RightChild;
}
else if (comparer < 0)
{
node = node.LeftChild;
}
else
{
node.Data = value;
return;
}
}
//找到插入位置,设置新插入节点的父节点为current
newNode.Parent = parent;
//比较插入节点的值跟插入位置的值的大小, 插入新节点
int comparer1 = mComparer.Compare(value, parent.Data);
if (comparer1 > 0)
{
parent.RightChild = newNode;
}
else if (comparer1 < 0)
{
parent.LeftChild = newNode;
}
//将它重新修整为一颗红黑树
InsertFixUp(newNode);
}
private void InsertFixUp(RedBlackTreeNode<T> newNode)
{
RedBlackTreeNode<T> parent = newNode.Parent; //插入节点的父节点
RedBlackTreeNode<T> gParent = null; //插入节点的祖父节点
//父节点的颜色是红色,并且不为空
while (IsRed(parent) && parent != null)
{
//获取祖父节点,这里不用判空,
//因为如果祖父节点为空,parent就是根节点,根节点是黑色,不会再次进入循环
gParent = parent.Parent;
//若父节点是祖父节点的左子节点
if (parent == gParent.LeftChild)
{
RedBlackTreeNode<T> uncle = gParent.RightChild; //获得叔叔节点
//case1: 叔叔节点也是红色
if (uncle != null && IsRed(uncle))
{
//把父节点和叔叔节点涂黑,祖父节点涂红
parent.Color = BLACK;
uncle.Color = BLACK;
gParent.Color = RED;
//把祖父节点作为插入节点,向上继续遍历
newNode = gParent;
parent = newNode.Parent;
continue; //继续while,重新判断
}
//case2: 叔叔节点是黑色,且当前节点是右子节点
if (newNode == parent.RightChild)
{
//从父节点处左旋
//当这种情况时,只能左旋,因为父亲节点和祖父节点变色,无论左旋还是右旋,都会违背红黑树的基本性质
RotateLeft(parent);
//当左旋后,红黑树变成case3的情况,区别就是插入节点是父节点
//所以,将父节点和插入节点调换一下,为下面右旋做准备
RedBlackTreeNode<T> tmp = parent;
parent = newNode;
newNode = tmp;
}
//case3: 叔叔节点是黑色,且当前节点是左子节点
// 父亲和祖父节点变色,从祖父节点处右旋
parent.Color = BLACK;
gParent.Color = RED;
RotateRight(gParent);
}
else
{
//若父节点是祖父节点的右子节点,与上面的完全相反
RedBlackTreeNode<T> uncle = gParent.LeftChild;
//case1: 叔叔节点也是红色
if (uncle != null & IsRed(uncle))
{
//把父节点和叔叔节点涂黑,祖父节点涂红
parent.Color = BLACK;
uncle.Color = BLACK;
gParent.Color = RED;
//把祖父节点作为插入节点,向上继续遍历
newNode = gParent;
parent = newNode.Parent;
continue;//继续while,重新判断
}
//case2: 叔叔节点是黑色的,且当前节点是左子节点
if (newNode == parent.LeftChild)
{
//从父节点处右旋
//当这种情况时,只能右旋,因为父亲节点和祖父节点变色,无论左旋还是右旋,都会违背红黑树的基本性质
RotateRight(parent);
RedBlackTreeNode<T> tmp = parent;
parent = newNode;
newNode = tmp;
}
//case3: 叔叔节点是黑色的,且当前节点是右子节点
// 父亲和祖父节点变色,从祖父节点处右旋
parent.Color = BLACK;
gParent.Color = RED;
RotateLeft(gParent);
}
}
//将根节点设置为黑色
mRoot.Color = BLACK;
}
public bool IsRed(RedBlackTreeNode<T> node)
{
if (node == null)
{
return false;
}
if (node.Color == RED)
{
return true;
}
return false;
}
public bool IsBlack(RedBlackTreeNode<T> node)
{
if (node == null)
{
return false;
}
if (node.Color == BLACK)
{
return true;
}
return false;
}
// 左旋转,逆时针旋转
/*************对红黑树节点x进行左旋操作 ******************/
/*
* 左旋示意图:对节点x进行左旋
* p p
* / /
* x y
* / \ / \
* lx y -----> x ry
* / \ / \
* ly ry lx ly
* 左旋做了三件事:
* 1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
* 2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
* 3. 将y的左子节点设为x,将x的父节点设为y
*/
private void RotateLeft(RedBlackTreeNode<T> x)
{
//1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
RedBlackTreeNode<T> y = x.RightChild;
x.RightChild = y.LeftChild;
if (y.LeftChild != null)
y.LeftChild.Parent = x;
//2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
y.Parent = x.Parent;
if (x.Parent == null)
{
mRoot = y; //如果x的父节点为空,则将y设为父节点
}
else
{
if (x == x.Parent.LeftChild) //如果x是左子节点
x.Parent.LeftChild = y; //则也将y设为左子节点
else
x.Parent.RightChild = y;//否则将y设为右子节点
}
//3. 将y的左子节点设为x,将x的父节点设为y
y.LeftChild = x;
x.Parent = y;
}
// 右旋转,顺时针旋转
/*************对红黑树节点y进行右旋操作 ******************/
/*
* 左旋示意图:对节点y进行右旋
* p p
* / /
* y x
* / \ / \
* x ry -----> lx y
* / \ / \
* lx rx rx ry
* 右旋做了三件事:
* 1. 将x的右子节点赋给y的左子节点,并将y赋给x右子节点的父节点(x右子节点非空时)
* 2. 将y的父节点p(非空时)赋给x的父节点,同时更新p的子节点为x(左或右)
* 3. 将x的右子节点设为y,将y的父节点设为x
*/
private void RotateRight(RedBlackTreeNode<T> y)
{
//1.将x的右子节点赋值给y的左子节点,同时将y赋值给x的右子节点的父节点(如果x的右子节点非空)
RedBlackTreeNode<T> x = y.LeftChild;
y.LeftChild = x.RightChild;
if (x.RightChild != null)
{
x.RightChild.Parent = y;
}
//2.如果y的父节点非空时,将y的父节点赋值给x的父节点,同时更新p的子节点为x
if (y.Parent != null)
{
x.Parent = y.Parent;
}
if (y.Parent.LeftChild == y)
{
y.Parent.LeftChild = x;
}
else
{
y.Parent.RightChild = x;
}
//3.将x的右子节点设为y,y的父节点设置为x
x.RightChild = y;
y.Parent = x;
}
public int Count
{
get
{
return CountLeafNode(mRoot);
}
}
private int CountLeafNode(RedBlackTreeNode<T> root)
{
if (root == null)
{
return 0;
}
else
{
return CountLeafNode(root.LeftChild) + CountLeafNode(root.RightChild) + 1;
}
}
public int Depth
{
get
{
return GetHeight(mRoot);
}
}
private int GetHeight(RedBlackTreeNode<T> root)
{
if (root == null)
{
return 0;
}
int leftHight = GetHeight(root.LeftChild);
int rightHight = GetHeight(root.RightChild);
return leftHight > rightHight ? leftHight + 1 : rightHight + 1;
}
public T Max
{
get
{
RedBlackTreeNode<T> node = mRoot;
while (node.RightChild != null)
{
node = node.RightChild;
}
return node.Data;
}
}
public T Min
{
get
{
if (mRoot != null)
{
RedBlackTreeNode<T> node = GetMinNode(mRoot);
return node.Data;
}
else
{
return default(T);
}
}
}
public void DelMin()
{
mRoot = DelMin(mRoot);
}
private RedBlackTreeNode<T> DelMin(RedBlackTreeNode<T> node)
{
if (node.LeftChild == null)
{
return node.RightChild;
}
node.LeftChild = DelMin(node.LeftChild);
return node;
}
public void Remove(T value)
{
mRoot = Delete(mRoot, value);
}
private RedBlackTreeNode<T> Delete(RedBlackTreeNode<T> node, T value)
{
if (node == null)
{
throw new ArgumentNullException();
}
int comparer = mComparer.Compare(value, node.Data);
if (comparer > 0)
{
node.RightChild = Delete(node.RightChild, value);
}
else if (comparer < 0)
{
node.LeftChild = Delete(node.LeftChild, value);
}
else
{
// a.如果删除节点没有子节点,直接返回null
// b.如果只有一个子节点,返回其子节点代替删除节点即可
if (node.LeftChild == null)
{
if (node.RightChild != null)
{
node.RightChild.Parent = node.Parent;
}
return node.RightChild;
}
else if (node.RightChild == null)
{
if (node.LeftChild != null)
{
node.LeftChild.Parent = node.Parent;
}
return node.LeftChild;
}
else
{
// c.被删除的节点“左右子节点都不为空”的情况
RedBlackTreeNode<T> child;
RedBlackTreeNode<T> parent;
bool color;
// 1. 先找到“删除节点的右子树中的最小节点”,用它来取代被删除节点的位置
// 注意:这里也可以选择“删除节点的左子树中的最大节点”作为被删除节点的替换节点
RedBlackTreeNode<T> replace = node;
replace = GetMinNode(replace.RightChild);
// 2. 更新删除父节点及其子节点
// 要删除的节点不是根节点
if (node.Parent != null)
{
// 要删除的节点是:删除节点的父节点的左子节点
if (node == node.Parent.LeftChild)
{
// 把“删除节点的右子树中的最小节点”赋值给“删除节点的父节点的左子节点”
node.Parent.LeftChild = replace;
}
else
{
// 把“删除节点的右子树中的最小节点”赋值给“删除节点的父节点的右子节点”
node.Parent.RightChild = replace;
}
}
else
{
// 要删除的节点是根节点
// 如果只有一个根节点,把mRoot赋值为null,这时replace为null
// 如果不止一个节点,返回根节点的右子树中的最小节点
mRoot = replace;
}
// 记录被删除节点的右子树中的最小节点的右子节点,父亲节点及颜色,没有左子节点
child = replace.RightChild;
parent = replace.Parent;
color = replace.Color;
// 3. 删除“被删除节点的右子树中的最小节点”,同时更新替换节点的左右子节点,父亲节点及颜色
// 替换节点 也就是 最小节点
if (parent == node)
{
// 被删除节点的右子树中的最小节点是被删除节点的子节点
parent = replace;
}
else
{
//如果最小节点的右子节点不为空,更新其父节点
if (child != null)
{
child.Parent = parent;
}
//更新最小节点的父节点的左子节点,指向最小节点的右子节点
parent.LeftChild = child;
//更新替换节点的右子节点
replace.RightChild = node.RightChild;
//更新删除节点的右子节点的父节点
node.RightChild.Parent = replace;
}
//更新替换节点的左右子节点,父亲节点及颜色
replace.Parent = node.Parent;
//保持原来位置的颜色
replace.Color = node.Color;
replace.LeftChild = node.LeftChild;
//更新删除节点的左子节点的父节点
node.LeftChild.Parent = replace;
//红黑树平衡修复
//如果删除的最小节点颜色是黑色,需要重新平衡红黑树
//如果删除的最小节点颜色是红色,只需要替换删除节点后,涂黑即可
//上面的保持原来位置的颜色已经处理了这种情况,这里只需要判断最小节点是黑色的情况
if (color == BLACK)
{
//将最小节点的child和parent传进去
RemoveFixUp(child, parent);
}
return replace;
}
}
return node;
}
private void RemoveFixUp(RedBlackTreeNode<T> node, RedBlackTreeNode<T> parent)
{
RedBlackTreeNode<T> brother;
// 被删除节点的右子树中的最小节点 不是 被删除节点的子节点的情况
while ((node == null || IsBlack(node)) && (node != mRoot))
{
if (parent.LeftChild == node)
{
//node是左子节点,下面else与这里的刚好相反
brother = parent.RightChild; //node的兄弟节点
if (IsRed(brother))
{
//case1: node的兄弟节点brother是红色的
brother.Color = BLACK;
parent.Color = RED;
RotateLeft(parent);
brother = parent.RightChild;
}
//case2: node的兄弟节点brother是黑色的,且brother的两个子节点也都是黑色的
//继续向上遍历
if ((brother.LeftChild == null || IsBlack(brother.LeftChild)) &&
(brother.RightChild == null || IsBlack(brother.RightChild)))
{
//把兄弟节点设置为黑色,平衡红黑树
brother.Color = RED;
node = parent;
parent = node.Parent;
}
else
{
//case3: node的兄弟节点brother是黑色的,且brother的左子节点是红色,右子节点是黑色
if (brother.RightChild == null || IsBlack(brother.RightChild))
{
brother.LeftChild.Color = BLACK;
brother.Color = RED;
RotateRight(brother);
brother = parent.RightChild;
}
//case4: node的兄弟节点brother是黑色的,且brother的右子节点是红色,左子节点任意颜色
brother.Color = parent.Color;
parent.Color = BLACK;
brother.RightChild.Color = BLACK;
RotateLeft(parent);
node = mRoot;
break;
}
}
else
{
//与上面的对称
brother = parent.LeftChild;
if (IsRed(brother))
{
// Case 1: node的兄弟brother是红色的
brother.Color = BLACK;
parent.Color = RED;
RotateRight(parent);
brother = parent.LeftChild;
}
// Case 2: node的兄弟brother是黑色,且brother的俩个子节点都是黑色的
if ((brother.LeftChild == null || IsBlack(brother.LeftChild)) &&
(brother.RightChild == null || IsBlack(brother.RightChild)))
{
//把兄弟节点设置为黑色,平衡红黑树
brother.Color = RED;
node = parent;
parent = node.Parent;
}
else
{
// Case 3: node的兄弟brother是黑色的,并且brother的左子节点是红色,右子节点为黑色。
if (brother.LeftChild == null || IsBlack(brother.LeftChild))
{
brother.RightChild.Color = BLACK;
brother.Color = RED;
RotateLeft(brother);
brother = parent.LeftChild;
}
// Case 4: node的兄弟brother是黑色的;并且brother的左子节点是红色的,右子节点任意颜色
brother.Color = parent.Color;
parent.Color = BLACK;
brother.LeftChild.Color = BLACK;
RotateRight(parent);
node = mRoot;
break;
}
}
}
//如果删除的最小节点的右子节点是红色,只需要替换最小节点后,涂黑即可
if (node != null)
{
node.Color = BLACK;
}
}
private RedBlackTreeNode<T> GetMinNode(RedBlackTreeNode<T> node)
{
while (node.LeftChild != null)
{
node = node.LeftChild;
}
return node;
}
// 中序遍历:首先遍历其左子树,然后访问根结点,最后遍历其右子树。
// 递归方法实现体内再次调用方法本身的本质是多个方法的简写,递归一定要有出口
public void ShowTree()
{
ShowTree(mRoot);
}
private void ShowTree(RedBlackTreeNode<T> node)
{
if (node == null)
{
return;
}
ShowTree(node.LeftChild);
string nodeColor = node.Color == RED ? "red" : "black";
string log;
if (node.Parent != null)
{
log = node.Data + " " + nodeColor + " parent= " + node.Parent.Data;
}
else
{
log = node.Data + " " + nodeColor + " parent= null";
}
//打印节点数据
Console.WriteLine(log);
ShowTree(node.RightChild);
}
}
public class RedBlackTreeNode<T>
{
//数据
public T Data { get; set; }
//左子节点
public RedBlackTreeNode<T> LeftChild { get; set; }
//右子节点
public RedBlackTreeNode<T> RightChild { get; set; }
//父节点
public RedBlackTreeNode<T> Parent { get; set; }
//该节点颜色
public bool Color { get; set; }
public RedBlackTreeNode(T value, bool color)
{
Data = value;
LeftChild = null;
RightChild = null;
Color = color;
}
}
}
红黑树的实现,是在前辈们写的基础上完善了,红黑树删除节点时,增加删除节点没有子结点和只有一个子节点的判断。
同时,对很多实现步骤增加了注释,让代码更加容易理解一些。
下面贴上前辈大神红黑树的详细实现:【数据结构和算法05】 红-黑树(看完包懂~)
附上:对二叉查找树和红黑树的测试用例:
using System;
namespace StructScript
{
public class TestBinaryTree
{
static void Main(string[] args)
{
BinaryTree<int> tree = new BinaryTree<int>();
//RedBlackTree<int> tree = new RedBlackTree<int>();
tree.Add(10);
tree.Add(2);
tree.Add(13);
tree.Add(40);
tree.Add(11);
tree.Add(15);
tree.Add(12);
//tree.Add(50);
//tree.Add(60);
//tree.Add(45);
tree.ShowTree();
Console.WriteLine("包含13吗?{0}", tree.Contains(13));
Console.WriteLine("最大值是{0}", tree.Max);
Console.WriteLine("最小值是{0}", tree.Min);
Console.WriteLine("元素总数是{0}", tree.Count);
Console.WriteLine("树的深度是{0}", tree.Depth);
tree.Remove(10);
tree.ShowTree();
Console.ReadLine();
}
}
}
红黑树的查找:
将要查找的value和节点的value比较,如果小于,那么就在Left Node节点查找;如果大于,则在Right Node节点查找,如果相等,更新Value。
红黑树的删除:
1.如果删除节点没有子节点,直接返回null
2.如果只有一个子节点,返回其子节点代替删除节点即可
3.当左右子节点都不为空时,找到其右子树中的最小节点,替换删除节点的位置
在实现原理上,红黑树的查找和删除,跟二叉查找树是一致的。不过,为了保证树在插入、删除完成之后,保持红黑树的平衡状态,需要实现更多更复杂的逻辑。
6.3树表查找算法——B树和B+树
B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。
B树定义:
B树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。
根节点至少有两个子节点
每个节点有M-1个key,并且以升序排列
位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
其它节点至少有M/2个子节点
B+树定义:
B+树是对B树的一种变形树,它与B树的差异在于:
- 有k个子结点的结点必然有k个关键码;
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
6.4树表查找算法——2-3查找树
2-3查找树的定义如下:
1)要么为空,要么:
2)对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比key要大。
3)对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
7.哈希查找
基本思想:哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
使用哈希查找有两个步骤:使用哈希函数将被查找的键转换为数组的索引。在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。所以哈希查找的第二个步骤就是处理冲突
处理哈希碰撞冲突。有很多处理哈希碰撞冲突的方法,本文后面会介绍拉链法和线性探测法。
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。7.1拉链法
将大小为M 的数组的每一个元素指向一个条链表,链表中的每一个节点都存储散列值为该索引的键值对C#算法实现:
namespace StructScript
{
/// <summary>
/// 哈希表的查找算法主要分为两步:
/// 第一步是用哈希函数将键转换为数组的一个索引,理想情况下不同的键都能转换为不同的索引值,但是实际上会有多个键哈希到到相同索引值上。
/// 因此,第二步就是处理碰撞冲突的过程。这里有两种处理碰撞冲突的方法:separate chaining(拉链法)和linear probing(线性探测法)。
/// 拉链法:
/// 将大小为M 的数组的每一个元素指向一个条链表,链表中的每一个节点都存储散列值为该索引的键值对,
/// </summary>
public class HashSearch1<T>
{
private int mCount;//散列表大小
private SequentialLinkedList<T>[] mHashArr;
//这个容量是为了测试方便,应根据填充数据,确定最接近且大于链表数组大小的一个素数
//并随着数据的添加,自动扩容
public HashSearch1() : this(997) { }
public HashSearch1(int m)
{
mCount = m;
mHashArr = new SequentialLinkedList<T>[m];
for (int i = 0; i < m; i++)
{
mHashArr[i] = new SequentialLinkedList<T>();
}
}
private int HashCode(T value)
{
return (value.GetHashCode() & 0x7fffffff) % mCount;
}
public void Add(T value)
{
//如果哈希出的索引一样,则依次添加到一个相同链表中
//添加的值,如果在链表中不存在,则依次在链表中添加新的数据
int hashCode = HashCode(value);
mHashArr[hashCode].Add(value);
}
public bool Contains(T value)
{
int hashCode = HashCode(value);
return mHashArr[hashCode].Contains(value);
}
}
}
附上:拉链法使用的单链表C#代码
using System;
using System.Collections;
using System.Collections.Generic;
namespace StructScript
{
public class SequentialLinkedList<T> : IEnumerable<T>
{
private Node fakehead = new Node(default(T), null);
private Node mFirst;
private int mCount;
public SequentialLinkedList()
{
mFirst = null;
mCount = 0;
}
public void Add(T value)
{
if (value == null)
{
throw new ArgumentNullException();
}
if (!Contains(value))
{
//这里每添加一个新的项,往前依次添加,新增项作为新的表头
//如果往后添加的话,需要遍历所有节点,在最后的位置添加新的项
mFirst = new Node(value, mFirst);
mCount++;
}
}
public bool Remove(T value)
{
if (value == null)
{
throw new ArgumentNullException();
}
fakehead.next = mFirst;
for (Node prev = fakehead; prev.next != null; prev = prev.next)
{
if (value.Equals(prev.next.value))
{
prev.next = prev.next.next;
mFirst = fakehead.next;
mCount--;
return true;
}
}
return false;
}
public void Clear()
{
for (Node current = mFirst; current != null; current = current.next)
{
Node tempNode = current;
tempNode.next = null;
tempNode.value = default(T);
}
mFirst = null;
mCount = 0;
}
public bool Contains(T value)
{
if (value == null)
{
throw new ArgumentNullException();
}
for (Node current = mFirst; current != null; current = current.next)
{
if (value.Equals(current.value))
{
return true;
}
}
return false;
}
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
public struct Enumerator : IEnumerator<T>
{
private SequentialLinkedList<T> list;
private int index;
private T current;
private Node node;
public Enumerator(SequentialLinkedList<T> list)
{
this.list = list;
index = 0;
current = default(T);
node = list.mFirst;
}
object IEnumerator.Current
{
get
{
if (index <= 0 || index > list.Count)
{
throw new IndexOutOfRangeException();
}
return current;
}
}
public T Current
{
get
{
if (index <= 0 || index > list.Count)
{
throw new IndexOutOfRangeException();
}
return current;
}
}
public void Dispose()
{
}
public bool MoveNext()
{
if (index >= 0 && index < list.Count)
{
if (node != null)
{
current = node.value;
node = node.next;
index++;
return true;
}
}
return false;
}
public void Reset()
{
index = 0;
current = default(T);
}
}
public int Count
{
get
{
return mCount;
}
}
private class Node
{
public T value;
public Node next;
public Node(T value, Node next)
{
this.value = value;
this.next = next;
}
}
}
}
7.2线性探测法
当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中的下一个位置即将索引值加1。
线性探测虽然简单,但是有一些问题,它会导致同类哈希的聚集。在存入的时候存在冲突,在查找的时候冲突依然存在。
C#算法实现:
namespace StructScript
{
/// <summary>
/// 哈希表的查找算法主要分为两步:
/// 第一步是用哈希函数将键转换为数组的一个索引,理想情况下不同的键都能转换为不同的索引值,但是实际上会有多个键哈希到到相同索引值上。
/// 因此,第二步就是处理碰撞冲突的过程。这里有两种处理碰撞冲突的方法:separate chaining(拉链法)和linear probing(线性探测法)。
/// 线性探测法:
/// 使用大小为M的数组来保存N个键值对,我们需要使用数组中的空位解决碰撞冲突
/// 当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中的下一个位置即将索引值加1
/// 线性探测虽然简单,但是有一些问题,它会导致同类哈希的聚集。在存入的时候存在冲突,在查找的时候冲突依然存在
/// </summary>
public class HashSearch2<T>
{
private int mCount = 16;//线性探测表的大小
private T[] mValues;
public HashSearch2()
{
mValues = new T[mCount];
}
private int HashCode(T value)
{
return (value.GetHashCode() & 0xFFFFFFF) % mCount;
}
public void Add(T value)
{
//当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中的下一个位置即将索引值加1
for (int i = HashCode(value); mValues[i] != null; i = (i + 1) % mCount)
{
//如果和已有的key相等,则用新值覆盖
if (mValues[i].Equals(value))
{
mValues[i] = value;
return;
}
//插入
mValues[i] = value;
}
}
public bool Contains(T value)
{
//当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中的下一个位置即将索引值加1
for (int i = HashCode(value); mValues[i] != null; i = (i + 1) % mCount)
{
if (value.Equals(mValues[i]))
{
return true;
}
}
return false;
}
}
}
总结:
在实现C#数据结构的七大查找算法时,遇到很多问题,也意识到自己的很多不足,最后一句感触比较深的话送给自己,也送给大家共勉,“纸上得来终觉浅,绝知此事要躬行。”互相交流,共同进步。
下一篇,应该是C#数据结构-八大排序算法的具体实现。
附录:
一些前辈大神的博客,从前辈大神那里学到很多。
寒江独钓 对数据结构和算法,有很深的见地和理解。
倪升武的博客 对红黑树有很深的理解,本文的红黑树实现就是在此基础上实现的。