基础知识和一些简单的数据结构的实现方式,四种排序方法,不一定能有所帮助?
数据结构:
数据结构是相互之间存在一种或者多种特定关系的数据元素的集合。再任何问题当中,数据元素之间都不是孤立的,而是存在着一定的关系,这种关系称为结构,根据数据元素之间关系的不同特性,通常有4类基本数据结构:
算法:
算法可以理解为有基本运算及规定的运算顺序所构成的完整解决问题的步骤。或者堪称按照要求设计好的有限的确切的计算序列,并且这样的步骤和序列可以解决一类问题。
算法和数据结构的关系
数据结构可以认为是数据在程序中的存储结构,和基本数据操作。
算法可以认为是依托数据结构来解决问题的,算法是基于数据结构的。
数据结构是问题的核心,是算法的基础。
算法的评价标准
1.运行的时间。即从开始运行到完成运行所用的时间。
2.占用的空间。即算法运行期间所占用的最大内存空间。
3.算法的正确性、可读性、健壮性。
运行时间越短、占用空间越小、可读性越高、健壮性越好,说明算法逻辑比较好。但是有的时候,我们也需要牺牲空间来换取时间,或者牺牲时间来换取空间。随着现代计算机的发展,内存越来越大,所以我们更偏向于运行时间。实际项目中,依照项目需求,寻求最佳的占比是最佳的解决方案。
1.线性结构
线性结构是最简单、最基本、最常用的数据结构。线性结构的特点是结构中的数据元素之间存在一对一的线性关系。这种一对一的关系指的是数据元素之间的位置关系,线性结构数据位置具有先后关系,一个接着一个排列的数据结构,类似排队。
顺序表:
顺序表是线性结构当中的一种数据结构方式。
优点:顺序表在逻辑上相邻的数据元素在内存上的位置也是相邻的,因此对顺序表进行查找的时候非常的方便。
缺点:同样因为顺序表数据元素在物理内存位置相邻的因素,导致我们插入数据、删除数据的时候,需要通过移动其他数据 来实现,导致效率低下。
C#当中List<>就是一个线性表,下边我们自己实现一个简单的线性表MyList,为了方便传代码,我将接口和类放在了同一个脚本中,这貌似不符合规范。?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 数据结构和算法
{
///
/// 自定义的一个List类,实现我们创建的接口
/// List实际上维护的是一个数组
/// 我们这里采用的是每次添加一个数据就扩容 1 的方式,这种方式效率不高但是简单
/// 建议维护数组的时候,每次扩容量按照项目需求设定,可以提高效率
///
class MyList : MyListInterface
{
private T[] myDatas;
private int capacity = 0;//容量
private int CurrentCapacity = -1;//当前储存位置下标
public MyList()
{
myDatas = new T[] { };
}
///
/// 索引器,根据索引获取数据
///
public T this[int index] => GetElem(index);
///
/// 获取List的长度
///
public int Length => GetLength();
///
/// 向MyList中添加数据
///
public void Add(T item)
{
//扩容,数据量大的情况下,建议改变逻辑,按照项目需求扩容
//这里没有判断数组容量,直接扩容 +1
T[] temp = myDatas;
myDatas = new T[++capacity];
if (CurrentCapacity == -1)
myDatas[++CurrentCapacity] = item;
else
{
for (int i = 0; i < temp.Length; i++)
{
myDatas[i] = temp[i];
}
myDatas[++CurrentCapacity] = item;
}
}
///
/// 清除数据
///
public void Clear()
{
myDatas = new T[] { };
CurrentCapacity = -1;
capacity = 0;
}
///
/// 根据索引删除数据
///
public void Delete(int index)
{
T[] temp = new T[Length - 1];
int tempNum = 0;
for (int i = 0; i < Length - 1; i++)
{
if (index == i)
{
tempNum = 1;
temp[i] = myDatas[i + tempNum];
}
else
{
temp[i] = myDatas[i+ tempNum];
}
}
myDatas = temp;
//删除完毕不要忘记把容量和下标修改
CurrentCapacity--;
capacity--;
}
///
/// 根据索引获取数据
///
public T GetElem(int i)
{
return myDatas[i];
}
///
/// 插入数据
///
public void Insert(T item, int index)
{
T[] temp = myDatas;
myDatas = new T[++capacity];
int tempNum = 0;
for (int i = 0; i < Length; i++)
{
if (index == i)
{
myDatas[i] = item;
tempNum = 1;
}
else
myDatas[i] = temp[i - tempNum];
}
}
///
/// 判断MyList是否为Null
///
public bool IsEmpty()
{
return myDatas.Length == 0;
}
///
/// 根据数据获得索引
///
public int Locate(T value)
{
for (int i = 0; i < myDatas.Length; i++)
{
if (myDatas[i].Equals(value))
return i;
}
return -1;
}
///
/// 获取数组长度
///
public int GetLength()
{
return myDatas.Length;
}
}
public interface MyListInterface
{
int Length { get; } //获取表长度
void Clear(); //清楚表元素
bool IsEmpty(); //判断是否为null
void Add(T item); //添加元素
void Insert(T item, int i); //通过索引插入元素
void Delete(int i); //通过索引删除表元素
T GetElem(int i); //通过索引获取表元素
T this[int index] { get; } //定义一个索引器 获取元素
int Locate(T value); //按照值查找
}
}
单链表:
单链表也是线性结构当中的一种数据结构方式。
优点:链表不要求逻辑上相邻的数据在物理内存上也相邻,更好的利用物理内存的碎片空间,因此对链表的插入和删除等操 作不需要移动数据,提高了运行速度。
缺点:无法像顺序表一样快速的读取数据
单链表的简单实现方式:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 数据结构和算法
{
class MyLinkList : MyListInterface
{
private Node headNode;//头节点,将通过此节点进行查找数据、删除数据、添加数据等操作
private Node endNode;//尾节点,方便存储避免循环查找为节点,加快效率
private int length = 0;
public MyLinkList()
{
headNode = null;
endNode = null;
}
public T this[int index] => GetElem(index);
public int Length => length;
///
/// 添加数据
///
public void Add(T item)
{
Node node = new Node(item);//创建一个节点,存入传进来的数据
if (headNode == null)
{
headNode = node;
}
else if (headNode != null && endNode == null)
{
endNode = node;
headNode.NextNode = endNode;
}
else
{
endNode.NextNode = node;
endNode = node;
}
length++;
}
///
/// 直接将头节点设置为null,剩下的交给GC
///
public void Clear()
{
length = 0;
headNode = null;
endNode = null;
}
///
/// 删除指定下标的数据
///
public void Delete(int index)
{
if (index >= Length || index < 0)
return;
Node temp = null;
if (index == Length-1) //删除末尾节点的方法
{
length--;
for (int i = 0; i < Length; i++)
{
temp = temp == null ? headNode : temp.NextNode;//找到前节点
}
temp.NextNode = null;
endNode = temp;
}
else if (index == 0)//说明要删除头节点
{
length--;
headNode = headNode.NextNode;
}
else
{
Node beforeTheNode = null; //要插入位置的上一个节点
for (int i = 0; i < index; i++)
{
beforeTheNode = beforeTheNode == null ? headNode : beforeTheNode.NextNode;//找到前节点
}
beforeTheNode.NextNode = beforeTheNode.NextNode.NextNode;//直接把前节点和后后节点相连
}
}
///
/// 根据下标获取数据
///
public T GetElem(int index)
{
Node temp = null;
for (int i = 0; i <= index; i++)
{
temp = temp == null ? headNode : temp.NextNode;
}
return temp != null ? temp.Data : default(T);
}
///
/// 插入数据
///
public void Insert(T item, int index)
{
if (index > Length|| index<0)
return;
else if (index == Length)//说明是添加数据,直接调用Add方法
Add(item);
else if(index==0)//说明要在头节点插入数据
{
length++;
Node value = new Node(item);
value.NextNode = headNode;
headNode = value;
}
else
{
length++;
Node value = new Node(item);
Node beforeTheNode = null; //要插入位置的上一个节点
Node afterTheNode = null; //要插入位置的下一个节点
for (int i = 0; i < index; i++)
{
beforeTheNode = beforeTheNode == null ? headNode : beforeTheNode.NextNode;//找到前节点
}
afterTheNode = beforeTheNode.NextNode;//从而获得后节点
beforeTheNode.NextNode = value; //连接
value.NextNode = afterTheNode; //连接
}
}
///
/// 判断是否为空
///
public bool IsEmpty()
{
return headNode == null ? true : false;
}
///
///根据数据找到下标
///
public int Locate(T value)
{
int index = -1;
Node temp = null;
for (int i = 0; i < Length; i++)
{
temp = temp == null ? headNode: temp.NextNode;
if (temp != null && temp.Data.Equals(value))
{
index = i;
break;
}
else if (temp==null)
return -1;
}
return index;
}
public override string ToString()
{
string Info = string.Empty;
for (int i = 0; i < Length; i++)
{
Info += " " + GetElem(i);
}
return Info;
}
}
public interface MyListInterface
{
///
/// 获取表长度
///
int Length { get; }
///
/// 清除表元素
///
void Clear();
///
///判断是否为null
///
bool IsEmpty();
///
/// 添加元素
///
void Add(T item);
///
/// 通过索引插入元素
///
void Insert(T item, int i);
///
/// 通过索引删除表元素
///
void Delete(int i);
///
/// 通过索引获取表元素
///
T GetElem(int i);
///
/// 定义一个索引器 获取元素
///
T this[int index] { get; }
///
/// 按照值查找
///
int Locate(T value);
}
}
栈
栈和队列是两种非常重要的数据结构,在软件设计中应用很多。栈和队列也是线性结构,线性表、栈、队列这三种数据结构的数据元素以及数据元素之间的逻辑关系完全相同,差别是栈和队列被一种规则所约束。
C#中提供了泛型的Stack
Stack stack = new Stack();
stack.Push("555"); //入栈
stack.Push("888");
stack.Push("888");
stack.Push("777");
var value = stack.Pop(); //出栈,但是会删除数据,返回被删除的数据
value = stack.Peek(); //取得栈顶的数据,但是不出栈(不删除数据)
int count = stack.Count;//取得栈中数据元素的数量
stack.Contains("555"); //栈中是否存在某数据
stack.Clear(); //清空栈中的数据
我们自己通过数组实现一个简单的栈:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 栈和队列
{
///
/// 栈的实现方式:我们可以维护一个数组(顺序栈),也可以维护一个链表
///
class MyStock : MyStockInterface
{
private T[] datas;
public int Count => datas.Length;
public MyStock()
{
datas = new T[] { };
}
public void Clear()
{
datas = new T[] { };
}
///
/// 数据是否存在
///
public bool Contains(T value)
{
for (int i = 0; i < datas.Length; i++)
{
if (value.Equals(datas[i]))
{
return true;
}
}
return false;
}
///
/// 获得数据,但是不出栈
///
public T Peek()
{
return datas[datas.Length - 1];
}
///
/// 获得栈顶数据并且出栈
///
public T Pop()
{
T[] temp = new T[datas.Length-1];
T value = datas[datas.Length - 1];
for (int i = 0; i < datas.Length-1; i++)
{
temp[i] = datas[i];
}
datas = temp;
return value;
}
///
///向栈内添加数据
///
public void Push(T value)
{
T[] temp = new T[datas.Length+1];
for (int i = 0; i < temp.Length; i++)
{
if (i == temp.Length - 1)
temp[i] = value;
else
temp[i] = datas[i];
}
datas = temp;
}
}
///
/// 首先我们定义一个接口来限定自写栈要实现的方法
///
interface MyStockInterface
{
int Count { get; } //获得数据大小
void Push(T value);//添加数据
void Clear(); //清空数据
bool Contains(T value); //栈中是否包含该数据
T Pop(); //取得并删除栈顶数据
T Peek(); //只取得栈顶数据
}
}
队列
C#中提供了泛型的Queue
Queue queue = new Queue();
//添加数据 入队
queue.Enqueue("我是第一位数据");
queue.Enqueue("我是第二位数据");
queue.Enqueue("我是第三位数据");
queue.Enqueue("我是第四位数据");
queue.Enqueue("我是第五位数据");
//获得数据 出队 会删除第一个元素
string value = queue.Dequeue();
//获得数据 不会删除第一个元素
value = queue.Peek();
//判断某数据是不是在队列中
bool isHave = queue.Contains("我是第三位数据");
//清空队列
queue.Clear();
//获得队列所储存的队列数量
int count = queue.Count;
排序
排序是把一个集合或者序列,按照某个数据项(排序项)重新排列成递增或者递减的序列。
作为排序依据的数据项称为排序项。
常用的排序方法:冒泡排序、插入法排序、选择法排序、快速排序(二分法排序)
using System;
namespace 栈和队列
{
class Program
{
static void Main(string[] args)
{
int[] sortInfo = new int[10] { 10,9,5,11,6,5,21,7,8,1 };
MyQueueSort(sortInfo,0, sortInfo.Length-1);
string temp = string.Empty;
foreach (var item in sortInfo)
{
temp += " " + item;
}
Console.WriteLine(temp);
Console.ReadLine();
}
///
/// 插入法排序:
/// 插入法排序的思想是, 从前往后排序,我们是取出一个值,插入到之前的排列好的序列之中,所以叫插入法排序
/// new int[10] { , 9,10,7,6,5,4,3,2,1 };
///
static void InsertionSort(int[] datas)
{
for (int i = 1; i < datas.Length; i++)
{
int fiducialData = datas[i];//首先获得一个基准值
bool IsTheMinimum = true;//用来判断我们的基准值在前序列中是不是最小值
//在这个循环中我们遍历 i 之前的数据,并将 i 之前的序列有序排列,以下将 i 之前排列好的序列称为前序列
for (int j = i-1; j >= 0; j--)
{
//此处分两种状态
//状态一:基准值是前有序序列中最小的一个,会频繁进入判断一,将前序列后移一位
//状态二:基准值的大小处于前序列的中间位置,会进入判断二,进入判断二之后,我们就可以终止循环了,因为此时基准值前边的数都比他小
if (datas[j] >= fiducialData)
{
//这里我们将前序列向后移动,当移动到 j<0 的时候,会跳出循环,此时并没有处理我们的基准值fiducialData,
//如何处理fiducialData呢,我们可以在这里将 fiducialData 赋值给datas[j] ,但是这样做会进行频繁的交换数据,造成效率低下
//我们可以设定一个bool值默认为:true,在else中将其设定为false,在跳出嵌套循环后,如果该bool值始终为true,说明我们的判断进入到最后一位,
//在循环外将基准值赋给前序列的第一位
datas[j + 1] = datas[j];
}
else
{
//进入该判断,说明我们的基准值的大小在前序列中处于中间部位
//将我们的基准值放在空出的位置上,将bool值IsTheMinimum设置为false,告诉循环外的判断基准值不是前序列的最小值
//然后就可以终止循环了,因为基准值前边都是比自己小的值,无需再做比对
datas[j + 1] = fiducialData;
IsTheMinimum = false;
break;
}
}
if (IsTheMinimum == true)
{
datas[0] = fiducialData;
}
}
}
///
/// 冒泡法排序:
/// 首先定义一个循环,遍历集合中的每一位数据元素
/// 然后再嵌套一个循环,将第一个循环中的值,在这个循环中和每一个值比对
/// 发现比自己小或者比自己大的数就交换
/// 循环结束之后,就按照要求排列了
///
static void BubbleSort(int[] datas)
{
for (int i = 0; i < datas.Length; i++)
{
for (int j = i+1; j < datas.Length; j++)
{
if (datas[i] > datas[j])
{
int temp = datas[j];
datas[j] = datas[i];
datas[i] = temp;
}
}
}
}
///
/// 选择法排序
/// 思相 : 利用循环找出集合中最小的一个数,将其放在首位,然后继续找下一个最小的数
///
static void SelectionSort(int[] datas)
{
for (int i = 0; i < datas.Length; i++)
{
int mainNum = datas[i]; //最小值
int mainNumIndex = i; //记录最小值的下标
for (int j = i+1; j < datas.Length; j++)
{
if(mainNum> datas[j])//发现比我们定义的最小值还小的值
{
mainNum = datas[j];//重新赋值最小值
mainNumIndex = j; //记录最小值的下标
}
}
//循环结束我们找到了最小值
//将集合的 i 位置和最小值的位置进行交换
datas[mainNumIndex] = datas[i];
datas[i] = mainNum;
}
}
///
/// 快速排序(二分法排序)
///
static void MyQueueSort(int[] values, int left, int right)
{
if (left < right)
{
int median = values[left];//递归当中,获取最左侧的值作为基准值,基准值被取出来了,原先基准值的位置就留下一个“坑”
int L = left;//从左往右遍历,直到碰到R
int R = right;//从右往左遍历,直到碰到L
//为什么在这里开一个循环,我们首先看以下该循环内部是如何工作的
while (L < R)
{
//循环一:
//new int[10] { 5, 8, 9, 6, 4, 10, 1, 2, 3, 7 }; 可以使用这个数组进行测试
//当遍历到有测的R==L的时候,说明找到了一个比median基准值小的数 放到了左侧
while (L < R)
{
if (values[R] < median)
{
//当我们找到一个比median小的数的时候,我们把找到的这个数values[R],放到取出基准值median后,留下的“坑”里,然后values[R]的位置就又形成一个“坑”
values[L] = values[R];
//此时右侧的坑需要找一个比基准值大的数字来填充
//所以我们终止这个循环,再开启一个循环寻找比基准值大的数
break;
}
else
{
R--;
}
}
//循环二:
//接下来就要从左往右寻找比median大的值,放到右侧,与上一个循环类似
while (L < R)
{
if (values[L] > median)
{
//同样当我们找到一个比median大的数的时候,我们把这个数填充到上一个循环中values[R]所留下的“坑”
values[R] = values[L];
//此时 values[L]填充到右侧的坑里,左侧又出现一个坑,我们现在又需要从右侧寻找一个比基准值小的数,放进左侧的坑里
//所以我们终止这个循环,重新返回上边的循环继续寻找,直到下标L == R 为止
break;
}
else
{
L++;
}
}
}
//当以上循环执行完毕,L==R 意味着坑的位置==R也==L,此时 L、R 左侧的值都比基准数小,右侧的值都比基准数大
//因此我们将基准数放置在L、R的位置上
values[L] = median;
//进行到这一步还没完,基准数的位置确认了,但是左右两侧的数字还是混乱的,大家伙可以注释掉下边的代码测试一下
//然后我们需要单向递归调用自身,把左侧的和右侧的传进去再次排序。
//一直这样递归调用,直到在下一个递归中 left == right 碰头,递归结束,说明所有的基准值,左侧都比自己小,右侧都比自己大,这样就有序排列了
MyQueueSort(values, left, R - 1);
MyQueueSort(values, L + 1, right);
}
}
}
}