算法的特征

算法有四大特征

1. 确定性:算法的每个步骤都是明确的,对结果的预期也是确定的。

2. 有穷性:算法必须由有限个步骤组成,必须有一个确定的结束条件。

3. 可行性:算法额每一个步骤都是可行的,只要一个步骤不行算法就是失败的。

4. 输入和输出:算法是要解决特定的问题,问题来源就是算法的输入,期望结果就是算法的输出。

算法的定义

算法是为了解决一个特定的问题而精心设计的一套数学模型以及在这套数学模型上的一系列操作步骤,

这些操作步骤将问题描述的输入数据逐步处理、转换,并最后得到一个确定的结果。

时间复杂度

时间频度

一个算法中的语句执行次数成为语句频度或时间频度。记作T(n)

时间复杂度

n称为问题的规模,n不断变化,T(n)也不断变化,时间复杂度就是变化时呈现的规律。

算法中执行次数是T(n),若有某个辅助函数f(n),使得n趋近于无限大时,T(n)/f(n)的极限值不等于零的常数,称fn(n)是T(n)的同数量级函数。

记作T(n)=O(f(n)),称O(f(n))为时间复杂度。例如:O(n^2)

按数量级递增排列,常见的时间复杂度:

  • 常数阶O(1)
  • 对数阶O(log2n)
  • 线性阶O(n)
  • 线性对数阶O(nlog2n)
  • 平方阶O(n^2)
  • 立方阶O(n^3)
  • k方阶O(n^k)
  • 指数阶O(2^n)

时间复杂度记忆方法

冒泡、选择、插入需要两个for,每次只关注一个元素,平均复杂度为O(n2) 。一遍找元素O(n),一遍找位置O(n)

快速、归并、希尔、堆基于二分思想,log以2为底,平均复杂度O(nlogn) 。一遍找元素O(n),一遍找位置O(logn)

稳定性记忆方法

排序算法的稳定性指:排序前后相同元素的相对位置不变,则称排序算法是稳定的。否则是不稳定的

log

log10100相当于问10的多少次方等于100。答案是2,因为10*10=100。

这里使用的是大O表示法讨论运行时间时,log指的都是log2。如果列表包含8个数字,使用二分查找,最多需要检查logn个元素。也就是log8=3。

记录一下log,举个例子:for(int j=1;j<n;j*=2)。这个循环的时间复杂度是:O(log2n)

j每循环一次乘以2;j初始化为1,循环x后为j=2^x;j>n时循环停止,2^x>n,此时x=log2n

二分查找

当列表是有序的时候,二分查找是一种非常好的查找方式。

def binary_search(list, item):
low = 0
high = len(list) - 1
while low <= high:
mid = int((low + high) / 2)
guess = list[mid]
if guess == item:
return mid
if guess > item:
high = mid - 1
else:
low = mid + 1
return None oriList = [1, 2, 3, 4, 5, 6, 7, 8]
print(binary_search(oriList, 7))

最多需要猜测的次数与列表长度相同,被称为线性时间(linear time)

二分查找的运行时间称为对数时间

大O表示法

特殊的表示法,指出了算法的速度有多快。

#

线性表

数据结构中最简单的基本数据结构就是线性表。最常见的线性表有四种:数组、链表、栈、队列

数组

一种相对比较简单的数据关系,所有数据存放在一片连续区域内。通过下标访问数组元素,可以进行插入、删除查找

数组直接访问没有开销,插入和删除操作需要移动数组元素,开销比较大。

因此在插入和删除操作比较频繁场合下,不适合使用数组。

数组中查找一个元素的时间复杂度是O(n),如果数组是有序的,使用二分查找,可以将时间复杂度降为O(logn)

链表

在长度不能确定的场合,一般采用链表的形式。链表有两部分组成,1存放实体数据,2.指针指向

单链表,指针指向后方。双链表,是由前后指针两个组成的。

链表的插入和删除,只需要修改指针指向即可完成。比数组的插入和删除效果高。

但是查询效率很低,需要从头到尾部遍历,时间复杂度是O(n)

可以将链表尾部节点的向后指针,指向链表头部节点,构成环形链表。从任何一个节点开始都可以遍历整个链表。

对于一些插入和删除操作比较少,查找、遍历操作比较多的场合,应优先选择可变长数组替代链表。

class Node():
def __init__(self, data):
self.data = data
self.next = None # 头插法
class SingnalNode():
def __init__(self):
self.current_node = None def add_node(self, data):
"""
头插法插入节点
:param data:
:return:
"""
node = Node(data)
node.next = self.current_node
self.current_node = node def append_node(self, data):
"""
尾插法插入节点
:param data:
:return:
"""
node = Node(data)
cur = self.current_node
# 遍历链表直到头节点处停止遍历
while cur:
if cur.next == None:
break
cur = cur.next
cur.next = node def travel(self):
"""
遍历链表
:return:
"""
cur = self.current_node
while cur:
print(cur.data)
cur = cur.next def is_empty(self):
"""
判断链表非空
:return:
"""
return self.current_node == None def get_lenth(self):
"""
获取链表的长度
:return:
"""
cur = self.current_node
count = 0
while cur:
count += 1
cur = cur.next
return count def insert_node(self, index, data):
"""
指定位置插入节点
:param index:
:param data:
:return:
"""
link_len = self.get_lenth()
if index == 0:
self.add_node(data)
elif index >= link_len:
self.append_node(data)
else:
cur = self.current_node
for i in range(1, index):
cur = cur.next
node = Node(data)
node.next = cur.next
cur.next = node def del_node(self, index):
"""
根据索引删除节点
:param index:
:return:
"""
# 找到前节点
cur = self.current_node
# 前驱节点
pre = None
count = 1
len_num = self.get_lenth()
while cur:
if index == 1:
self.current_node = cur.next
break
if count == index and count < len_num:
pre.next = cur.next
break
if count >= len_num:
pre.next = None
break
count += 1
pre = cur
cur = cur.next def del_node(self, index):
"""
根据索引删除节点
:param index:
:return:
"""
# 找到前节点
cur = self.current_node
if index == 1:
self.current_node = cur.next
return
# 前驱节点
pre = None
len_num = self.get_lenth()
for i in range(1, len_num):
if i == index:
break
pre = cur
cur = cur.next
pre.next = cur.next if __name__ == "__main__":
test = SingnalNode()
list_data = [1, 2, 3]
for i in list_data:
test.add_node(i)
test.travel()
test.del_node(4)
test.travel()

栈是一种特殊的线性表,只能在表的一端插入和删除数组元素,分别称为:入栈,出栈。遵循后入先出原则

class Stack(object):
"""栈"""
def __init__(self):
self.items = [] def is_empty(self):
"""判断是否为空"""
return self.items == [] def push(self, item):
"""加入元素"""
self.items.append(item) def pop(self):
"""弹出元素"""
return self.items.pop() def peek(self):
"""返回栈顶元素"""
return self.items[len(self.items) - 1] def size(self):
"""返回栈的大小"""
return len(self.items) if __name__ == "__main__":
stack = Stack()
stack.push("hello")
stack.push("world")
stack.push("itcast")
print(stack.size())
print(stack.peek())
print(stack.pop())

队列

一种特殊的线性表,普通的队列只能在表的一端插入数据,在另一端删除数据,不能再其他地方插入和删除。

插入和删除动作,分别成为“入队”和“出队”。遵循先进先出原则

class Queue(object):
"""队列""" def __init__(self):
self.items = [] def is_empty(self):
return self.items == [] def enqueue(self, item):
"""进队列"""
self.items.insert(0, item) def dequeue(self):
"""出队列"""
return self.items.pop() def size(self):
"""返回大小"""
return len(self.items) if __name__ == "__main__":
q = Queue()
q.enqueue("hello")
q.enqueue("world")
q.enqueue("itcast")
print(q.size())
print(q.dequeue())

排序

简易桶排序

import numpy as np

oriList = np.random.randint(0, 1000, 50)
resultList = []
T = [0] * (max(oriList) + 1)
for i in range(len(oriList)):
T[oriList[i]] += 1
for i in range(0, len(T) - 1):
for j in range(0, T[i]):
resultList.append(i) print(resultList)

时间复杂度:O(N) 线性阶

数字间隔不大,使用一组数据来当做桶,进行插入排序。桶排序主要是利用下标的输出,然后根据数值循环下标。

冒泡

import numpy as np

oriList = np.random.randint(0, 1000, 50)
n = len(oriList) - 1
for i in range(n):
for j in range(n, i, -1):
if oriList[j - 1] > oriList[j]:
S = oriList[j]
oriList[j] = oriList[j - 1]
oriList[j - 1] = S
print(oriList)

时间复杂度:O(N2) 平方阶

双向冒泡

import numpy as np

oriList = np.random.randint(0, 1000, 50)
n = len(oriList)
st = -1
swapped = True
while st < n and swapped:
n -= 1
st += 1
swapped = False
for j in range(st, n):
if oriList[j] > oriList[j + 1]:
S = oriList[j]
oriList[j] = oriList[j + 1]
oriList[j + 1] = S
swapped = True
for j in range(n - 1, st - 1, -1):
if oriList[j] > oriList[j + 1]:
S = oriList[j]
oriList[j] = oriList[j + 1]
oriList[j + 1] = S
swapped = True print(oriList)

时间复杂度:O(N2) 平方阶

梳排序

保持间距并不断减少,初始设置列表长度,每次会除以损耗因子。间距可以四舍五入,不断重复,直到间距变成1

import numpy as np

oriList = np.random.randint(0, 1000, 50)
n = len(oriList)
swaps = 1
while n != 1 or swaps == 0:
n = int(n / 1.3)
if n < 1:
n = 1
i = 0
swaps = 0
while i + n < len(oriList):
if oriList[i] > oriList[i + n]:
S = oriList[i]
oriList[i] = oriList[i + n]
oriList[i + n] = S
swaps = 1
i += 1 print(oriList)

时间复杂度:O(Nlog2N) 线性对数阶

圈排序

import numpy as np

oriList = np.random.randint(0, 1000, 50)

for i in range(len(oriList)):
item = oriList[i]
pos = i
swapped = True
while i != pos or swapped:
to = 0
swapped = False
for j in range(len(oriList)):
if j != i and oriList[j] < item:
to += 1
if pos != to:
while pos != to and item == oriList[to]:
to += 1
temp = oriList[to]
oriList[to] = item
item = temp
pos = to print(oriList)

不稳定的排序,理论上是最优的比较算法。把数列分解为圈,可以分别旋转得到排序结果,与其他排序不同的是,元素不会被放入数组的任何位置。如果在正确位置则不动,否则只会写入一次

时间复杂度:O(N2) 平方阶

堆排序

从数据集构建一个数据堆,提取最大元素,放到有序数列末尾。然后重新构造新的数据堆,一直到没有数据位置。输入插入排序

import numpy as np

def heapSort(list):
for i in range(int((len(list) - 1) / 2), -1, -1):
adjust(list, i, len(list) - 1)
for i in range(len(list) - 1, 0, -1):
S = list[i]
list[i] = list[0]
list[0] = S
adjust(list, 0, i - 1)
return list def adjust(list, i, m):
temp = list[i]
j = i * 2 + 1
while j <= m:
if j < m and list[j] < list[j + 1]:
j += 1
if temp < list[j]:
list[i] = list[j]
i = j
j = 2 * i + 1
else:
j = m + 1
list[i] = temp oriList = np.random.randint(0, 1000, 50)
print(heapSort(oriList))

时间复杂度:O(Nlog2N)

插入排序

插入排序的原理是构造一个有序数列,对未排序的数据,从后向前扫描,找到相应的位置并插入。需要反复把排序元素逐步向右挪位,为最新元素提供插入空间

import numpy as np

oriList = np.random.randint(0, 1000, 50)
for i in range(len(oriList)):
val = oriList[i]
j = i - 1
done = False
while not done:
if oriList[j] > val:
oriList[j + 1] = oriList[j]
j -= 1
if j < 0:
done = True
else:
done = True
oriList[j + 1] = val print(oriList)

时间复杂度:O(N2) 平方阶

奇偶排序

通过比较相邻的奇偶数进行排序,对存在错误的顺序进行交换。并一直重复这个过程,直到列表有序

import numpy as np

oriList = np.random.randint(0, 1000, 50)
sorted = False
while not sorted:
sorted = True
for i in range(1, len(oriList) - 1, 2):
if oriList[i] > oriList[i + 1]:
S = oriList[i + 1]
oriList[i + 1] = oriList[i]
oriList[i] = S
sorted = False
for i in range(0, len(oriList) - 1, 2):
if oriList[i] > oriList[i + 1]:
S = oriList[i + 1]
oriList[i + 1] = oriList[i]
oriList[i] = S
sorted = False print(oriList)

时间复杂度:O(N2) 平方阶

快速排序

快速排序会把集合分成两个集合,并选择一个元素作为基准。把小于基准的数据排到基准前面,大于放到后面

import numpy as np

def QuickSort(list, left, right):
right = right == 0 and len(list) - 1 or right
i, j = left, right
x = list[int((left + right) / 2)]
while i <= j:
while list[i] < x:
i += 1
while list[j] > x:
j -= 1
if i <= j:
S = list[j]
list[j] = list[i]
list[i] = S
i += 1
j -= 1
if left < j:
QuickSort(list, left, j)
if right > i:
QuickSort(list, i, right)
return list oriList = np.random.randint(0, 1000, 50)
print(QuickSort(oriList, 0, 0))

时间复杂度:O(Nlog2N) 线性对数

选择排序

在未排序的列表中找到最小或最大的元素,存放在排序序列的起始位置,然后再从剩余的排序元素中继续找寻最小元素放到末尾

import numpy as np

oriList = np.random.randint(0, 1000, 50)
for i in range(len(oriList)):
min = i
for j in range(i + 1, len(oriList), 1):
if oriList[j] < oriList[min]:
min = j
S = oriList[min]
oriList[min] = oriList[i]
oriList[i] = S print(oriList)

时间复杂度:O(N2) 平方阶

希尔排序

通过将比较的全部元素分成几个区域来提升插入排序的性能。可以让一个元素一次性地朝最终位置前进一大步。然后步伐越来越小,最后就是普通的插入排序。

import numpy as np

oriList = np.random.randint(0, 1000, 50)
h = int(len(oriList) / 2)
while h > 0:
for i in range(h, len(oriList)):
temp = oriList[i]
if temp < oriList[i - h]:
for j in range(0, i, h):
if temp < oriList[j]:
temp = oriList[j]
oriList[j] = oriList[i]
oriList[i] = temp
h = int(h / 2) print(oriList)

时间复杂度:O(Nlog2N)

#

去重排序

例子:将序列进行去重复操作,并且进行排序。限制时间1秒。序列数字区间很大。

数字区间很大的话,就不可以使用桶排序了。那么我们就使用快速排序,然后在输出的时候,做一些手脚。

快速排序结束后,输出数组使用下面代码来进行去重复

if (list[i] != list[i - 1])

#

拉马车计算

规则跟拉马车一样,发牌后计算谁获胜。

思路:使用队列,来模拟玩家。使用栈模拟牌桌。使用桶排序模拟是否存在牌。出牌后结果两种,出列入列。根据出列入列计算。

使用while循环到手牌为空,其中赢牌的话讲Stack中的牌,添加到Queue队尾。

static void Main(string[] args)
{
new Program().quicksort("", "");
Console.ReadKey();
}
void quicksort(string texthe, string textha)
{
//初始化两个队列来存放
Queue<char> quHe = new Queue<char>(texthe);
Queue<char> quHa = new Queue<char>(textha);
//定义栈代表牌桌
Stack<char> stack = new Stack<char>();
//使用桶排序来存储出牌统计
int[] book = new int[];
//循环执行出牌
int i = ;
while (quHe.Count > && quHa.Count > )
{
Console.WriteLine("玩了{0}局", ++i);
if (i > ) {
Console.WriteLine("大于1000局,默认为平局");
return;
}
quickStack(quHe,book,stack);
quickStack(quHa, book, stack);
}
if (quHe.Count == )
Console.WriteLine("A获胜");
else
Console.WriteLine("B获胜");
}
void quickStack(Queue<char> quHa,int[] book,Stack<char> stack)
{
//出牌
char t = quHa.Peek();
//判断是否可以赢牌
if (book[int.Parse(t.ToString())] == ) //不能赢牌,入栈
{
quHa.Dequeue();
stack.Push(t);
book[int.Parse(t.ToString())]++;
}
else
{
quHa.Enqueue(quHa.Dequeue());
while (stack.Count != && stack.Peek() != t)
{
char tt = stack.Pop();
quHa.Enqueue(tt);
book[int.Parse(tt.ToString())]++;
}
}
if (quHa.Count == )
return;
}

穷举法

穷举法的基本思想就是“有序的去尝试每一种可能”。分别实验每个数,完成等式。代码略

深度优先搜索

深度优先搜索(Depth First search,DFS)的关键在于解决,当下该如何去做。至于下一步如何去做,是和当下如何去做一样的。所以我们主要是写当前怎么做的代码。

下面的代码就是深度优先搜索的基本模型,

void dfs(int step)
{
//判断边界
//判断每一种可能
for (int i = ; i <= n; i++)
{
//继续下一步
dfs(step + );
}
//返回
}

输入一个数N,输出1~N的全排列。

思路:设1,2,3三个牌,1,2,3三个箱子。约定顺序:每个箱子都先放1,然后2,最后3. 使用for循环约定,设为i。箱子设为book

第一重循环,因为BooK是空的,输出1。然后将箱子递增,递归到第二重循环。

第二重循环,因为book1是有值,所以i=1跳过,进入i=2。箱子递增,到第三重循环。

第三重循环,i=3。箱子递增到第四重循环。

第四重循环,箱子数大于传递过来的牌数。输出现有集合的内容,并且return;到第三重循环。

第三重循环,book3的值拿出来,然后return;到第二重循环。

第二重循环,book2的值拿出来,继续执行约定,i=3.然后在箱子递增,进入第三重循环。

以此类推,直到第一重循环结束,即可。

class Program
{
static void Main(string[] args)
{
Program pro = new Program();
pro.dfs();
Console.ReadKey();
} public readonly int[] book; //使用过的集合
public readonly int[] hezi; //排列集合
public readonly int n; //数量
public Program(int numberN)
{
n = numberN;
book = new int[numberN + ]; hezi = new int[numberN + ];
}
//深度优先搜索(Depth First Search,DFS)
void dfs(int t, int step = )
{
//step是盒子,如果盒子大于传递过来数量,那么进行打印
if (step == n + )
{
//分别打印盒子里的数据
for (int i = ; i <= n; i++)
{
Console.Write(hezi[i]);
}
Console.WriteLine("");
//返回跳出递归
return;
}
//123都试一遍
for (int i = ; i <= n; i++)
{
//试1,2,3那个有
if (book[i] == )
{
hezi[step] = i; //放入盒子
book[i] = ; //记录
dfs(t + , step + ); //执行下一个盒子
book[i] = ; //撤销记录
}
}
//此时会进行跳出操作,如上例子不包括打印是三重循环。
//第三重输出后撤回第三个数,然后跳出到第二重循环。此时第二重循环是执行到2的,当跳出以后会将2撤回,并且进行3的循环。
//这时会输出3,然后进入三重循环,输出2
return;
}
}

深度优先

使用深度优先,进行寻路

将不同的走法,代表不同的可能性。当前走的点,如果与终点吻合,当做边界。执行寻路的时候,需要判断不是障碍物,并且没有走过

class Program
{
static void Main(string[] args)
{
Program pro = new Program();
Console.ReadKey();
}
public Dictionary<string, string> text = new Dictionary<string, string>();
public List<string> lx = new List<string>(), lx2 = new List<string>();
public int h, l, p, q, min = int.MaxValue;
public int[,] a, book;
public Program()
{
//设置行走对应名称
text.Add("0,1", "向右"); text.Add("1,0", "向下"); text.Add("0,-1", "向左"); text.Add("-1,0", "向上");
text.Add("1,1", "右下"); text.Add("1,-1", "左下"); text.Add("-1,1", "左上"); text.Add("-1,-1", "右上");
//设置边界
h = ; l = ;
//设置已走路径
book = new int[h + , l + ];
//设置迷宫
a = new int[h + , l + ];
int[,] b = { { , , , }, { , , , }, { , , , }, { , , , }, { , , , } };
for (int i = ; i < h; i++)
{
for (int j = ; j < l; j++)
{
a[i + , j + ] = b[i, j];
}
}
//设置起点,标记在此起点
book[, ] = ;
//设置终点
p = ; q = ;
//进行寻路
dfs(, , );
Console.WriteLine(min);
int oldx = , oldy = ;
foreach (var item in lx2)
{
string[] s = item.Split(',');
int newx = int.Parse(s[]), newy = int.Parse(s[]);
Console.WriteLine(text[(newx - oldx) + "," + (newy - oldy)]);
oldy = newy;oldx = newx;
}
}
//深度优先搜索(Depth First Search,DFS)
void dfs(int x, int y, int step)
{
//定义向右、向下、向左、向上的移动。左侧为x轴,右侧为y轴
int[,] next = new int[, ] { { , }, { , }, { , - }, { -, }, { , }, { , - }, { -, }, { -, - } };
int tx, ty, k;
//判断边界,是否到达
if (x == p && y == q)
{
if (step < min)
{
min = step;
lx2 = lx.ToList();
}
return;
}
//判断每一种可能,枚举4种走法
for (k = ; k <= ; k++)
{
//计算下一步的坐标
tx = x + next[k, ];
ty = y + next[k, ];
//判断是否越界,超过边界
if (tx < || tx > h || ty < || ty > l)
{
continue;
}
//不是障碍物,并且没有走过
if (a[tx, ty] == && book[tx, ty] == )
{
book[tx, ty] = ;
//记录路线
lx.Add(tx + "," + ty);
//继续下一步
dfs(tx, ty, step + );
book[tx, ty] = ;
lx.Remove(tx + "," + ty);
}
}
return;
}
}

深度优先寻路

广度优先搜索

广度优先(Breadth First Search,BFS)也称宽度优先搜索。在迷宫的例子中,也可以使用广度优先来实现。

在深度优先里,是遍历了所有的行走可能性,使用函数递归实现。广度优先是通过一层一层扩展的方法来找到目标点的。

class note
{
internal int x; //横坐标
internal int y; //纵坐标
internal int s; //步数
internal note f; //父节点编号
}
private static void BFS()
{
int h = , l = , p = , q = , flag = , tx, ty;
int[,] a = new int[h + , l + ], book = new int[h + , l + ];
//设置步数列队
Queue<note> que = new Queue<note>();
DiTu(h, l, a); //绘制地图
//定义向右、向下、向左、向上的移动。左侧为x轴,右侧为y轴
int[,] next = new int[, ] { { , }, { , }, { , - }, { -, } };
//起始位置加入队列
que.Enqueue(new note() { x = , y = , s = });
book[, ] = ;
while (que.Count != )
{
//枚举四个方向
for (int k = ; k < ; k++)
{
tx = que.Peek().x + next[k, ];
ty = que.Peek().y + next[k, ];
//判断是否边界
if (tx < || tx > h || ty < || ty > l)
continue;
//判断是否障碍物或者已在路径中
if (a[tx, ty] == && book[tx, ty] == )
{
//标记走过
book[tx, ty] = ;
//插入新的点到队列中
que.Enqueue(new note { x = tx, y = ty, f = que.Peek(), s = que.Peek().s + });
}
if (tx == p && ty == q)
{
flag = ;
break;
}
}
if (flag == )
break;
que.Dequeue();
}
while (que.Count > )
{
que.Dequeue();
}
Console.WriteLine(que.Peek().s);
List<note> resultList = new List<note>();
note result = que.Peek();
while (result != null)
{
resultList.Add(result);
result = result.f;
if (result.f == null)
break;
}
resultList.Reverse();
foreach (var item in resultList)
{
Console.WriteLine(item.x + "-" + item.y);
}
} private static void DiTu(int h, int l, int[,] a)
{
//绘制地图
int[,] b = { { , , , }, { , , , }, { , , , }, { , , , }, { , , , } };
for (int i = ; i < h; i++)
{
for (int j = ; j < l; j++)
{
a[i + , j + ] = b[i, j];
}
}
}

广度优先搜索

深度优先遍历

深度优先遍历的主要思想是:

首先以一个未被访问过的顶点作为起始顶点,沿当前顶点的边走到未访问过的顶点。

当没有未访问过的顶点,则返回上一个顶点,直到所有的顶点都被访问过。

关于存放无向图,点边。可以使用二维数组来存放。1 表示有边,0 表示本身位置,∞ 表示没有边。

这种存储图的方式称为:图的邻接矩阵存储法

下面用代码来解决,深度优先遍历图表。顺序为 1 2 4 3 5

public Program()
{
//初始化二维矩阵
for (int i = ; i <= ; i++)
{
for (int j = ; j <= ; j++)
{
if (i == j)
{
e[i, j] = ;
}
else
{
e[i, j] = int.MaxValue;
}
}
}
//读入顶点之间的边
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
//从1号城市出发
book[] = ;
dfs();
}
int[] book = new int[];
int[,] e = new int[, ];
int sum;
void dfs(int cur)
{
Console.Write(cur + " ");
sum++;
int i;
if (sum == )
{
return;
}
for (i = ; i <= ; i++)
{
if (e[cur, i] == && book[i] == )
{
book[i] = ;
dfs(i);
}
}
return;
}

深度遍历

广度优先遍历

上图如果使用广度优先遍历,那么顺序为 1 2 3 5 4

首先找起点,将1号起点放入队列,然后将2,3,5号依次放入队列中。在将2号顶点相邻的4号,放入队列。

public Program()
{
//初始化二维矩阵
for (int i = ; i <= ; i++)
{
for (int j = ; j <= ; j++)
{
if (i == j)
{
e[i, j] = ;
}
else
{
e[i, j] = int.MaxValue;
}
}
}
//读入顶点之间的边
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
//从1号城市出发
book[] = ;
dfs();
}
int[] book = new int[];
int[,] e = new int[, ];
void dfs(int cur)
{
int head = ;
IList<int> que = new List<int>();
//放入顶点
que.Add(cur);
while (head <= )
{
cur = que[head - ];
for (int i = ; i <= ; i++) //从1~n依次尝试
{
//判断是否有边,并且是否访问过
if (e[cur, i] == && book[i] == )
{
//如果从顶点cur到顶点i有边,并且顶点i没有被访问过,则入队
que.Add(i);
book[i] = ;
}
//判断边界
if (que.Count >= )
{
break;
}
}
head++;
}
foreach (var item in que)
{
Console.Write(item.ToString() + " ");
}
}

广度优先遍历

最短路径-深度优先

城市地图,实际上就是图遍历的一种应用。深度优先遍历,就可以实现城市地图功能。

需要制定地址,公路,路线。这三个数据,就跟图表中的点,边类似。

int min = int.MaxValue, n;
int[] book;
int[,] e;
public Program()
{
n = ;
book = new int[n + ];
e = new int[, ];
for (int i = ; i <= n; i++)
{
for (int j = ; j <= n; j++)
{
if (i == j)
{
e[i, j] = ;
}
else
{
e[i, j] = int.MaxValue;
}
}
}
SetE(, , );
SetE(, , );
SetE(, , );
SetE(, , );
SetE(, , );
SetE(, , );
SetE(, , );
SetE(, , );
book[] = ;
dfs(, );
Console.WriteLine(min);
} private void SetE(int a, int b, int c)
{
e[a, b] = c;
e[b, a] = c; //单向路则注释此行
}
void dfs(int cur, int dis)
{
if (dis > min) return;
if (cur == n)
{
if (dis < min) min = dis;
return;
}
for (int j = ; j <= n; j++)
{
if (e[cur, j] != int.MaxValue && book[j] == )
{
book[j] = ;
dfs(j, dis + e[cur, j]);
book[j] = ;
}
}
return;
}

最短路径

最少转机-广度优先

如果要求到达目的地,最少换乘。就可以使用广度优先,来实现此功能。广度优先更适用于所有边都一样长。

struct note
{
public int x; //城市编号
public int s; //转机次数
};
public Program()
{
Queue<note> que = new Queue<note>();
int[] book = new int[];
int n = , m = , start = , end = , cur, flag = ;
for (int i = ; i <= n; i++)
{
for (int j = ; j <= n; j++)
{
if (i == j)
e[i, j] = ;
else
e[i, j] = int.MaxValue;
}
}
SetE(, ); SetE(, );
SetE(, ); SetE(, );
SetE(, ); SetE(, );
SetE(, );
que.Enqueue(new note { x = start, s = });
book[start] = ;
int title = ;
while (que.Count > )
{
cur = que.Peek().x;
for (int j = ; j <= n; j++)
{
if (e[cur, j] != int.MaxValue && book[j] == )
{
que.Enqueue(new note { x = j, s = que.Peek().s + });
book[j] = ;
title++;
}
if (que.Count > && que.AsEnumerable().ElementAt(que.Count - ).x == end)
{
flag = ;
break;
}
}
if (flag == )
break;
que.Dequeue();
}
Console.WriteLine(que.AsEnumerable().ElementAt(que.Count - ).s);
}
int[,] e = new int[, ];
private void SetE(int a, int b)
{
e[a, b] = ; e[b, a] = ;
}

最少转机

多源最短路径-Floyd(佛洛依德)算法

多源最短路径,有Robert W.Floyd(罗伯特.弗洛伊德)与1962年发表。

单向公路,求两个城市之间最短路径。也就是两个点之间的最短路径。可以使用图的邻接矩阵存储法来表示。

如果要让任意两点之间的距离变短,只能引用第三个点(顶点K),并通过k进行中转。这样才能缩短原来从顶点a到b的路径。

中转点k,有时候不只通过一个点,而是经过两个点或更多的点。

如果只允许经过1号顶点,求任意两点之间的最短路径。只需要判断:e[i,1] + e[1,j] 是否比 e[i,j] 小,即可。

也就是 1号点到b的距离 + a到1号点的距离 < a到b点的距离。

以此类推,经过1 和 2 两个顶点的写法,如下

public Program()
{
for (int i = ; i <= n; i++)
{
for (int j = ; j <= n; j++)
{
if (e[i, j] > e[i, ] + e[, j])
e[i, j] = e[i, ] + e[, j];
}
}
for (int i = ; i <= n; i++)
{
for (int j = ; j <= n; j++)
{
if (e[i, j] > e[i, ] + e[, j])
e[i, j] = e[i, ] + e[, j];
}
}
}

综上所示,如果允许所有顶点作为中转,那么Floyd-Warshall算法核心代码如下

public Program()
{
for (int k = ; k <= n; k++)
{
for (int i = ; i <= n; i++)
{
for (int j = ; j <= n; j++)
{
if (e[i, j] > e[i, k] + e[k, j])
{
e[i, j] = e[i, k] + e[k, j];
}
}
}
}
}

Floyd-Warshall

单源最短路径-Dijkstra(迪杰斯特拉)算法

单源最短路径,由Edsger Wybe Dijkstra与1959年提出

指定一个点到其余各个顶点的最短路径,也需要使用二维数组e来存储顶点之间的关系。

还需要用一个一维数组dis来存储1号顶点到其余各个顶点的初始路程,将此时dis数组中的值称为最短路径的“估计值”

接下来通过估计值进行发散搜索,那么1点到3点的距离则为

dis[3] = 12,dis[2] + e[2,3] = 1+9=10,dis[3] > dis[3] + e[2,3]。因此要更新dis[3]的值。这个过程叫“松弛”

这个时候因为dis[3]已经确定最短路径,那么变为“确定值”

Dijkstra算法的主要思想:每次找到离源点最近的一个点,然后以该点为中心进行扩展,最终得到所有点的最短路径。步骤如下:

1.将所有的顶点分为两部分:已知最短路径的顶点集合P和未知最短路径的顶点集合Q。使用book数组记录那些点在集合P中。如果book[i]为1则表示这个顶点在集合P中,如果book[i]为0则表示这个顶点在集合Q中。

2.设置源点s到自己的最短路径为0,即dis[s] = 0、若存在有源点能直接到达的顶点i,则把dis[i] 设为 e[s,j]。同时把所有其他的顶点的最短路径设为∞。

3.在集合Q的所有顶点中选择一个离源点最近的顶点u,加入到集合P中。并考察所有以点u为起点的边,对每一条边进行松弛操作。例如存在一条从u到v的边,那么可以通过将边u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u] + e[u,v]。如果这个值比目前已知的dis[v] 值要小,我们可以使用新值来代替当前dis[v]中的值。

4.重复第三步,如果集合Q为空,算法结束。

public Program()
{
int[,] e = new int[, ];
int[] dis = new int[], book = new int[];
int n = ;
for (int i = ; i <= n; i++)
{
for (int j = ; j <= n; j++)
{
if (i == j)
e[i, j] = ;
else
e[i, j] = int.MaxValue;
}
}
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ; e[, ] = ;
e[, ] = ;
for (int i = ; i <= n; i++)
{
dis[i] = e[, i];
}
for (int i = ; i <= n; i++)
{
book[i] = ;
}
book[] = ; //Dijkstra算法核心语句
int min = , u = ;
for (int i = ; i <= n - ; i++)
{
min = int.MaxValue;
for (int j = ; j <= n; j++)
{
if (book[j] == && dis[j] < min)
{
min = dis[j];
u = j;
}
}
book[u] = ;
for (int v = ; v <= n; v++)
{
if (e[u, v] < int.MaxValue)
{
if (dis[v] > dis[u] + e[u, v])
dis[v] = dis[u] + e[u, v];
}
}
}
for (int i = ; i <= n; i++)
{
Console.WriteLine(dis[i]);
}
}

Dijkstra

完美的最短路算法-Bellman.Ford(贝尔曼福特)算法

Dijkstra算法虽然很好,但是无法解决边的权值为负数的问题。而BellmanFord算法无论从思想上还是代码上都堪称完美的最短路算法。

核心代码只有4行,并且可以完美的解决带有负权边的图。

for (int k = ; k <= n - ; k++)
{
for (int i = ; i <= m; i++)
{
if (dis[v[i]] > dis[u[i]] + w[i])
{
dis[v[i]] = dis[u[i]] + w[i];
}
}
}

外循环一共循环了n-1次,n为顶点的个数。内循环循环了m次,m为边的个数,枚举每一条边。dis数组作用和Dijkstra算法一样,用来记录源点到各个顶点的最短路径。

u,v,m三个数组用来记录边的信息。例如第i条边存储在u[i],v[i],w[i]中,表示从顶点u[i]到顶点v[i]这条边,权值为w[i] 。

最后的if 和Disjkstra的松弛操作一样,把所有的边都松弛一遍。

因为最短路径上最多有n-1条边,因此BellmanFord算法最多有N-1个阶段。在每一个阶段,我们对每一条边都要执行松弛操作。

每一次松弛操作,就会有一些顶点已经求得最短路,将估计值变为确定值。此后这些顶点的最短路会一直不变。

在前K个阶段结束后,就已经找到了从源点发出到达各个顶点的最短路。直到进行完n-1个阶段后,辨得出了最多经过n-1条边的最短路。

public Program()
{
int[] dis = new int[], v = new int[], u = new int[], w = new int[];
int n = , m = , check, flag;
Queue<string> que = new Queue<string>();
que.Enqueue("2,3,2"); que.Enqueue("1,2,-3"); que.Enqueue("1,5,5");
que.Enqueue("4,5,2"); que.Enqueue("3,4,3");
//读入边
for (int i = ; i <= m; i++)
{
string[] result = que.Dequeue().Split(',');
u[i] = int.Parse(result[]);
v[i] = int.Parse(result[]);
w[i] = int.Parse(result[]);
}
//初始化数据
for (int i = ; i <= n; i++)
{
dis[i] = ;
}
dis[] = ; for (int k = ; k <= n - ; k++)
{
check = ;
//第一轮松弛
for (int i = ; i <= m; i++)
{
if (dis[v[i]] > dis[u[i]] + w[i])
{
dis[v[i]] = dis[u[i]] + w[i];
check = ;
}
}
//检查数组dis是否有更新
if (check == ) break;
}
//检查负权回路
flag = ;
for (int i = ; i <= m; i++)
{
if (dis[v[i]] > dis[u[i]] + w[i])
flag = ;
} if (flag == )
Console.WriteLine("此图含有负权回路");
else
{
for (int i = ; i <= n; i++)
{
Console.Write(dis[i] + " ");
}
}
}

Bellman-Ford

二叉树

二叉树是一种特殊的树。二叉树的特点是每个结点最多有左右两个子节点。

二叉树的使用范围最广,一棵多叉树也可以转换为二叉树。二叉树中还有两种特殊的二叉树,叫做满二叉树和完全二叉树。

满二叉树指二叉树中每个结点都有两个子节点。完全二叉树指除了最右边位置上有一个或者几个结点缺少外,其他事丰满的。

堆是一种特殊的完全二叉树。如果所有父节点都比子节点小,我们成为最小堆。反之,成为最大堆。

假如有14个数,我们找出这14个数中最小的数。最简单的办法就是遍历所有的数,用一个循环就可以解决。

现在我们需要删除其中最小的数,并添加一个新数。再求14个数中最小的一个数。就可以使用最小堆。

当一个数被放置到堆顶时,如果不符合最小堆的特性,则需要将这个数向下调整,直到找到合适的位置为止。

#

C#算法知识点记录的更多相关文章

  1. JavaScript算法与数据结构知识点记录

    JavaScript算法与数据结构知识点记录 zhanweifu

  2. C#知识点记录

    用于记录C#知识要点. 参考:CLR via C#.C#并发编程.MSDN.百度 记录方式:读每本书,先看一遍,然后第二遍的时候,写笔记. CLR:公共语言运行时(Common Language Ru ...

  3. DB知识点记录

    DB知识点记录 分页 SqlServer:ROW_NUMBER () over (ORDER BY ID) AS RN, MySql:limit Oracle:ROWNUM AS RN 数据表的基本结 ...

  4. 【千纸诗书】—— PHP/MySQL二手书网站后台开发之知识点记录

    前言:使用PHP和MySQL开发后台管理系统的过程中,发现有一些通用的[套路小Tip],这里集中记录一下.结合工作中ing的后台业务,我逐渐体会到:除了技术知识外.能使用户体验好的“使用流程设计”积累 ...

  5. 计算机二级C语言选择题错题知识点记录。

    计算机二级C语言选择题错题知识点记录. 1,在数据流图中,用标有名字的箭头表示数据流.在程序流程图中,用标有名字的箭头表示控制流. 2,结构化程序设计的基本原则:自顶向下,逐步求精,模块化,限制使用g ...

  6. spring mvc开发过程知识点记录

    给一个客户做的一个小项目,需求就是输入类似一个短网址http://dd.yy/xxxx然后跳转到另外一个域名下的图书文件.(实际很多短网址站都提供API供调用吧,不过客户需求是他自己建立一个短网址服务 ...

  7. javascript知识点记录(1)

    javascript一些知识点记录 1.substring,slice,substr的用法 substring 和slice 都有startIndex 和 endIndex(不包括endInex),区 ...

  8. ML,DL核心数学及算法知识点总结

    ML,DL核心数学及算法知识点总结:https://mp.weixin.qq.com/s/bskyMQ2i1VMNiYKIvw_d7g

  9. PID算法知识点博文收藏记录

    https://blog.csdn.net/Uncle_GUO/article/details/51367764 https://blog.csdn.net/HandsomeHong/article/ ...

随机推荐

  1. Asp.Net MVC+BootStrap+EF6.0实现简单的用户角色权限管理8

    接下来做的是对页面的增删改查与页面与页面按钮之间的联系.先上代码和页面效果 using AuthorDesign.Web.App_Start.Common; using System; using S ...

  2. Spring中多配置文件以及寻觅引用其他bean的方式

    Spring多配置文件有什么好处? 按照目的.功能去拆分配置文件,可以提高配置文件的可读性与维护性,如将配置事务管理.数据源等少改动的配置与配置bean单独分开. Spring读取配置文件的几种方式: ...

  3. Windows Server 2012 虚拟化实战:存储(一)

    在计算机世界我们随处可以见的一种方法,那就是抽象.1946年冯诺依曼提出了计算机的基本结构包含:计算器,存储器和I/O设备.这可能是对计算机这一新生事物最重要的一次抽象,它直接影响了今后几十年计算机软 ...

  4. 迅为最新推出iTOP-6818开发平台无缝支持4418开发板

    iTOP-6818开发板是一款四核ARM 八核开发板与iTOP-4418开发板完全兼容,CPU主频1.4GHz,内存1GB DDR3(2GB可选),存储16GB EMMC,板载千兆以太网,GPS,WI ...

  5. hammer.js的六大事件

    1.Pan事件:在指定的dom区域内,一个手指放下并移动事件,即触屏中的拖动事件.这个事件在触屏开发中比较常用: Panstart 拖动开始 Panmove 拖动过程 Panend 拖动结束 Panc ...

  6. FineReport实现根据点击次数奇偶性排序的方法

    使用FineReport报表软在进行排序的时,很多时候您可能想实现根据点击的次数进行升降序排序,也就是说点击第一次点击标题升序排序,再次点击就降序,以此类推,而不是通过选择升序进行升序排列,选择降序进 ...

  7. UrlEncode 和 HtmlEncode

    UrlEncode 是将指定的字符串按URL编码规则,包括转义字符进行编码.

  8. 数-模(D/A)转换器

    将实现数字信号到模拟信号的转换电流称为数模(D/A)转换器,简称为DAC(Digital - Analog Convert). 目前常见的D/A转化器中,有:权电阻网络D/A转换器.倒T型电阻网络D/ ...

  9. 利用setns()将进程加入一个新的network namespace

    1.首先使用docker创建一个容器,并获取该容器的network namespace monster@monster-Z:~$ docker run -itd --name test ubuntu ...

  10. UVALive3902 Network[贪心 DFS&&BFS]

    UVALive - 3902 Network Consider a tree network with n nodes where the internal nodes correspond to s ...