归并排序 & 计数排序 & 基数排序 & 冒泡排序 & 选择排序 ----> 内部排序性能比较
2.3 归并排序
接口定义:
int merge(void* data, int esize, int lpos, int dpos, int rpos,
int (*compare)(const void* key1, const void* key2));
返回值:成功 0;失败 -1。
int merge_sort(void* data, int size, int esize,
int lpos, int rpos, int (*compare)(const void* key1, const void* key2));
返回值:成功 0;失败 -1。
算法描述:
归并排序是另一种利用分治法排序的算法,其首先将数据集二等分为左右两个区间,分别在左右区间上递归地使用归并排序算法,然后将排序后的两个区间合并为一个有序区间。与快速排序一样,它依赖于元素之间的比较。归并排序的归并过程就是合并两个已经排好序的数据集合,此时对需要合并的元素遍历一次即可,非常高效,因此,归并排序在所有的情况下都能达到快速排序的平均性能。但是,其主要的缺点是排序过程需要额外的存储空间来支持,因为合并过程不能在无序数据集本身中进行,因此需要两倍于无序数据集的空间来运行算法,这也就极大地降低了实际中使用归并排序的频率,反而由快速排序取代它的工作。接口中的lpos、dpos、rpos分别表示一个区间的左端、中间、右端位置。
算法分析:
归并排序本质上就是将一个无序数据集分割成许多只包含一个元素的集合,然后按序将这些小集合合并为大集合,直到生成一个最大的有序数据集为止。由于归并过程需要额外的存储空间,所以merge_sort()要为合并过程分配足够多的内存,在排序结束时,将归并后的结果复制到data中。归并的关键部分就是merge()函数,其余部分都可以递归地完成。merge将data中lpos到dpos,dpos + 1到rpos这两个有序小区间合并为data中的lpos到rpos这个有序区间中,中间使用了额外的存储空间m。由于归并元素需要不断地二分区间,因此,需要lgn级分割。对于两个包含p和q个元素的有序区间的合并,由于需要遍历全部元素,因此需要的合并时间为O(p + q)。在每个分割级别下,恰好都需要合并所有元素一次(即需要遍历全部n个元素一次),因此,归并算法的时间复杂度为O(nlgn),此外,在不进行算法改进的情况下,需要两倍于无序数据集的存储空间,因此可以认为是O(n)。
算法实现:
/**
merge():
return: success 0; fail -1
*/
int merge(void* data, int esize, int lpos, int dpos, int rpos,
int (*compare)(const void* key1, const void* key2))
{
char* pd = (char*)data;
char* m;
int pl, pr, pm; /* 初始化指示器 */
pl = lpos; /* pl指向分割后的左分区 */
pr = dpos + ; /* pr指向分割后的右分区 */
pm = ; /* pm指向合并后的分区 */ /* 分配归并后结果的存储空间 */
if((m = (char*)malloc((rpos - lpos + ) * esize)) == NULL)
return -; /* 两个区间中仍然有需要合并的元素 */
while(pl <= dpos || pr <= rpos)
{
if(pl > dpos)
{
/* 左分区元素已经全部合并 */
while(pr <= rpos) /* 此时将右分区剩下的元素全部接在m的后边 */
{
memcpy(&m[pm * esize], &pd[pr * esize], esize);
pr++;
pm++;
}
continue;
}
else if(pr > rpos)
{
/* 右分区元素已经全部合并 */
while(pl <= dpos) /* 此时将左分区剩下的元素全部接在m的后边 */
{
memcpy(&m[pm * esize], &pd[pl * esize], esize);
pl++;
pm++;
}
continue;
} /* 将下一个有序集合添加到合并结果中 */
if(compare(&pd[pl * esize], &pd[pr * esize]) < )
{
memcpy(&m[pm * esize], &pd[pl * esize], esize);
pl++;
pm++;
}
else
{
memcpy(&m[pm * esize], &pd[pr * esize], esize);
pr++;
pm++;
}
} /* 复制合并结果到data中,然后释放m */
memcpy(&pd[lpos * esize], m, (rpos - lpos + ) * esize); /* 释放空间 */
free(m); return ;
}
/**
merge_sort():
return: success 0; fail -1
*/
int merge_sort(void* data, int size, int esize,
int lpos, int rpos, int (*compare)(const void* key1, const void* key2))
{
int dpos;
if(lpos < rpos)
{
/* 计算中间位置 */
dpos = (int)((lpos + rpos - ) / ); /* 递归地对左分区进行归并排序 */
if(merge_sort(data, size, esize, lpos, dpos, compare) < )
return -; /* 递归地对右分区进行归并排序 */
if(merge_sort(data, size, esize, dpos + , rpos, compare) < )
return -; /* 合并已经排好序的左右分区 */
if(merge(data, esize, lpos, dpos, rpos, compare) < )
return -;
} return ;
}
2.4 计数排序
接口定义:
int counts_sort(int* data, int size, int pnumbers);
返回值:成功 0;失败 -1。
算法描述:
计数排序是一种高效的线性排序,该算法不需要进行元素比较操作,而是通过计算数据集中元素出现的次数来确定该元素在有序集合中的正确偏移位置,其效率一般高于基于比较操作的O(nlgn)。接口中的pnumbers表示数据集中可能出现的元素个数,为最大值加1,比如:最大值为9意味着可能有0~9存在,因此pnumbers设为10。计数排序需要利用一个索引数组来记录各个元素的出现次数,如此,也就限制了计数排序只能对”可计算元素”进行排序。data数据集中的size个元素经过counts_sort()后将变得有序。
算法分析:
计数排序本质上就是通过计算无序数据集中每个元素出现的次数,将其转换为有序数据集中的偏移量来指导排序过程的。设计时需要定义一个索引数组counts,索引本身代表数据集中可能出现的值。而通过与前一个索引元素的相加即可得到当前索引的在有序数据集中的偏移量(注:如果有多个重复的元素,则为最后一个位置的偏移量)。接着,将索引元素按照偏移量插入到temp中,插入后对应的偏移量减1,表示已插入了一个重复的元素,再一次插入该元素时偏移指针向前移动一个单位。由于算法中包含三个循环,其中两个都为O(n),一个为O(p),因此时间复杂度为O(n + p);由于用到了额外的数组temp与counts,因此需要多分配一些内存空间,大致可以记为O(n)。
算法实现:
/**
counts_sort():
return: success 0; fail -1
*/
int counts_sort(int* data, int size, int pnumbers)
{
int* counts;
int* temp;
int value, i; /* 分配计数数组counts的存储空间 */
if((counts = (int*)malloc(pnumbers * sizeof(int))) == NULL)
return -; /* 分配存储排序后元素的临时空间 */
if((temp = (int*)malloc(size * sizeof(int))) == NULL)
return -; /* 初始化counts */
for(value = ; value < pnumbers; value++)
counts[value] = ; /* 计算每个元素出现的次数 */
for(i = ; i < size; i++)
counts[data[i]] = counts[data[i]] + ; /* 更新counts数组,使得其值代表对应元素在排序后的偏移量 */
for(i = ; i < pnumbers; i++)
counts[i] = counts[i] + counts[i - ]; /* 使用更新后counts中的偏移量计算每个元素的正确位置 */
for(i = size - ; i >= ; i--)
{
temp[counts[data[i]] - ] = data[i]; /* 多个重复的值总是从后向前依次放置 */
printf("temp[%d] stores %d\n", counts[data[i]] - , data[i]);
counts[data[i]] = counts[data[i]] - ;
} /* 将排序后的数据集复制到data中 */
memcpy(data, temp, size * sizeof(int)); free(temp);
free(counts); return ;
}
2.5 基数排序
接口定义:
int radix_sort(int* data, int size, int bits, int radix);
返回值:成功 0;失败 -1。
算法描述:
基数排序是另外一种高效的线性排序方法。其将数据按位分离,并从数据的最低有效位到最高有效位依次进行排序,从而得到有序数据集合。但是对某个位进行排序时必须选择稳定的排序算法。基数排序并不局限于对整型数据进行排序,凡是将元素分割为整型的,就可以使用它。基数的选择依赖于数据本身,例如以28为基对字符串进行排序,以10为基对整型进行排序...。bits表示每个待排元素包含的位数,radix表示基数。
算法分析:
基数排序实质上就是对元素的每一位进行计数排序,因此其时间复杂度为O(b(n + r)),其中n为数据集中元素个数,r为基数radix,b为每个元素的位数bits;其空间复杂度与计数排序一样,需要额外的temp和counts,大致可以记为O(n)。
算法实现:
/**
radix_sort():
return: success 0; fail -1
*/
int radix_sort(int* data, int size, int bits, int radix)
{
int* counts;
int* temp;
int bit, bitvalue, value, i, index; /* 分配计数数组counts的存储空间 */
if((counts = (int*)malloc(radix * sizeof(int))) == NULL)
return -; /* 分配存储排序后元素的临时空间 */
if((temp = (int*)malloc(size * sizeof(int))) == NULL)
return -; /* 从最低有效位到最高有效位依次进行计数排序 */
for(bit = ; bit < bits; bit++)
{
/* 分离有效位 */
bitvalue = (int)pow((double)radix, (double)bit); /* 初始化counts */
for(value = ; value < radix; value++)
counts[value] = ; /* 计算每个元素出现的次数 */
for(i = ; i < size; i++)
{
index = (int)(data[i] / bitvalue) % radix;
counts[index] = counts[index] + ;
} /* 更新counts数组,使得其值代表对应元素在排序后的偏移量 */
for(i = ; i < radix; i++)
counts[i] = counts[i] + counts[i - ]; /* 使用更新后counts中的偏移量计算每个元素的正确位置 */
for(i = size - ; i >= ; i--)
{
index = (int)(data[i] / bitvalue) % radix;
temp[counts[index] - ] = data[i]; /* 多个重复的值总是从后向前依次放置 */
printf("temp[%d] stores %d\n", counts[index] - , data[i]);
counts[index] = counts[index] - ;
} /* 将排序后的数据集复制到data中 */
memcpy(data, temp, size * sizeof(int));
} free(temp);
free(counts); return ;
}
2.6 冒泡排序和选择排序
还有两种比较常用的排序方法,分别为冒泡排序和选择排序,如下简单描述:
冒泡排序:
基本思想就是两两比较相邻的元素,如果逆序,则交换位置,最后最小的元素就像气泡一样浮到最上面。为了避免对已然有序的数据集一直执行比较操作,可以设置标志位flag控制循环,如果某次循环存在元素交换,设flag = 1,表明下次仍然需要继续循环比较;如果false = 0,表明剩余元素已然有序,此时排序工作结束。改进后的冒泡排序最好情况下,只需要n-1次比较,时间复杂度为O(n);在数据集为逆序时,情况最糟糕,为O(n2)。
算法实现:
/**
bubble_sort():
return: success 0; fail -1
*/
int bubble_sort(void* data, int size, int esize,
int (*compare)(const void* key1, const void* key2))
{
char* pd = (char*)data;
void* ptemp;
int num, i;
int flag = ; if((ptemp = (char*)malloc(esize)) == NULL)
return -; for(num = ; (num < size - ) && (flag == ); num++) /* 循环次数 */
{
flag = ; /* 初始化为 0 */
for(i = size - ; i >= num; i--)
{
if(compare(&pd[i * esize], &pd[(i + ) * esize]) > )
{
memcpy(ptemp, &pd[i * esize], esize);
memcpy(&pd[i * esize], &pd[(i + ) * esize], esize);
memcpy(&pd[(i + ) * esize], ptemp, esize);
flag = ;
}
}
}
free(ptemp); return ;
}
选择排序:
基本思想就是在第num次循环中找出最小的元素与pd[num]进行交换,num范围为[0, size -1),因为只剩下最后一个元素时,不必再进行选择了。第num次循环中,需要size – 1 – num次比较操作。因此,其时间复杂度为O(n2)。
算法实现:
/**
select_sort():
return: success 0; fail -1
*/
int select_sort(void* data, int size, int esize,
int (*compare)(const void* key1, const void* key2))
{
char* pd = (char*)data;
void* ptemp;
int num, i, min; if((ptemp = (char*)malloc(esize)) == NULL)
return -; for(num = ; num < size - ; num++)
{ min = num; /* 假设索引为num的元素值最小 */
for(i = num + ; i < size; i++)
{
if(compare(&pd[min * esize], &pd[i * esize]) > )
min = i;
}
if(compare(&min, &num) != )
{
memcpy(ptemp, &pd[num * esize], esize);
memcpy(&pd[num * esize], &pd[min * esize], esize);
memcpy(&pd[min * esize], ptemp, esize);
}
}
free(ptemp); return ;
}
3 性能分析
为了对不同排序算法的性能进行定量分析,采用随机产生的数据集:规模分别为1000、5000、10000、50000。同一规模下的输入数据集分别有随机排序、正序排序、逆序排列三种,输出均为升序排序。运行环境为:处理器 奔腾T4300,主频2.1GHz,内存 2G,编译器 GUN gcc 4.7.1。
表1 随机序列排序耗时
|
执行时间 /s |
|||||||
|
规模 /个 |
插入排序 |
快速排序 |
归并排序 |
计数排序 |
基数排序 |
冒泡排序 |
选择排序 |
|
1000 |
0.006000 |
0.002000 |
0.001000 |
5.206000 |
5.206000 |
0.016000 |
0.007000 |
|
5000 |
0.149000 |
0.014000 |
0.005000 |
18.132000 |
71.043000 |
0.443000 |
0.133000 |
|
10000 |
0.561000 |
0.023000 |
0.011000 |
34.643000 |
141.395000 |
1.791000 |
0.547000 |
|
50000 |
14.320000 |
0.126000 |
0.057000 |
185.015000 |
888.030000 |
45.331000 |
13.638000 |
表2 正序序列排序耗时
|
执行时间 /s |
|||||||
|
规模 /个 |
插入排序 |
快速排序 |
归并排序 |
计数排序 |
基数排序 |
冒泡排序 |
选择排序 |
|
1000 |
0.000000 |
0.002000 |
0.001000 |
2.973000 |
9.086000 |
0.010000 |
0.005000 |
|
5000 |
0.000000 |
0.012000 |
0.004000 |
16.025000 |
63.358000 |
0.085000 |
0.130000 |
|
10000 |
0.001000 |
0.024000 |
0.010000 |
32.207000 |
128.935000 |
0.222000 |
0.522000 |
|
50000 |
0.004000 |
0.130000 |
0.044000 |
178.152000 |
938.841000 |
1.680000 |
13.425000 |
表3 逆序序列排序耗时
|
执行时间 /s |
|||||||
|
规模 /个 |
插入排序 |
快速排序 |
归并排序 |
计数排序 |
基数排序 |
冒泡排序 |
选择排序 |
|
1000 |
0.010000 |
0.002000 |
0.002000 |
3.247000 |
10.031000 |
0.022000 |
0.007000 |
|
5000 |
0.285000 |
0.014000 |
0.004000 |
17.477000 |
70.101000 |
0.561000 |
0.194000 |
|
10000 |
1.155000 |
0.025000 |
0.008000 |
35.476000 |
141.936000 |
2.239000 |
0.751000 |
|
50000 |
29.223000 |
29.223000 |
0.048000 |
198.325000 |
806.668000 |
58.161000 |
20.617000 |
如表1所示,在数据集规模比较小时,选择任何一种算法来进行排序,其性能都不会有太大的差别;随着输入数据规模的扩大,快速排序和归并排序表现优秀,插入排序和选择排序性能受到一定的影响;冒泡排序不适合用于大规模排序任务。表2为对正序数据集进行排序的时间开销,一般而言,好的排序算法对于已经有序的序列应该立刻给出响应,不需要再次重新排序,插入算法完美地解决了这个问题,其他算法如快速排序、归并排序、冒泡排序等也比较适合这种工作;对于数据规模比较大时,选择排序会做一些无用的工作。表3中,归并排序对于任何规模的逆序数据集性能优越,显然插入排序、快速排序、选择排序、冒泡排序不适合大规模的逆序数据集。总结一下,无论是随机、正序、逆序、甚至不同规模,归并排序总能得到最佳的性能,而快速排序只是对于大规模逆序集不太适应,其他情况同样优秀。同样值得一提的是,在预先得知数据序列大部分有序的情况下,插入排序也是一个很好的选择。从表1,表2,表3中得出一个结论,对于随机、正序、逆序数据集,计数排序和基数排序的性能几乎总是一样的,这是由于这两种排序的方式并不依赖于比较操作,因此,其并不受序列是否有序的影响,只依赖于数据集的规模大小。之所以上述实验中这两种排序方式的时间开销最大,是因为对于整型我们习惯性地选择了10作为基数,但是计算机却是以2进制为基的,因此可以采用2的幂次作为基数,这样在分离不同的位时就可以避免乘除操作而采用位运算以提高速度。但是计数排序和基数排序的复杂度是线性增长的,如果数据规模超级大,此时和其他排序算法的性能差距必然会越来越小,甚至超过其他算法。
对算法的定性评价主要是基于两条标准:时间复杂度和空间复杂度。如果算法所需的额外空间不依赖于数据集的规模,则认为其辅助空间为O(1),此时称之为原地排序,比如:插入排序、冒泡排序、选择排序;而对于归并排序、计数排序、基数排序而言,也需要O(n)量级的辅助空间;对快速排序进行改进后,可能需要O(lgn)的空间即可完成工作了。下面总结了不同算法的定性比较:
|
排序算法 |
空间复杂度S(n) |
最坏时间复杂度T(n) |
平均时间复杂度T(n) |
|
插入排序 |
O(1) |
O(n2) |
O(n2) |
|
快速排序 |
O(lgn) |
O(n2) |
O(nlgn) |
|
归并排序 |
O(n) |
O(nlgn) |
O(nlgn) |
|
计数排序 |
O(n) |
O(n + p) |
O(n + p) |
|
基数排序 |
O(n) |
O(b(n + r)) |
O(b(n + r)) |
|
冒泡排序 |
O(1) |
O(n2) |
O(n2) |
|
选择排序 |
O(1) |
O(n2) |
O(n2) |
4 结论
实际中如何选择合适的算法时,不能单纯地依赖于算法的复杂度分析,更重要的是要考虑具体应用到哪个数据集上,实验数据表明,在数据规模比较小时,任何算法都可以看作是等价的。但是对于大规模数据集,必须全面衡量排序算法的选择。同时,一个算法的设计不能仅仅依赖于实现功能即可,对于关键部分必须进行优化设计才能很好地发挥其作用。
附表(上述排序中用到的额外操作)
/**
比较两个int的大小(正序)
*/
int compare_int(const void* key1, const void* key2){
return *(const int*)key1 < *(const int*)key2 ? - :
*(const int*)key1 > *(const int*)key2 ? : ;
} /**
比较两个int的大小(逆序)
*/
int compare_int_reverse(const void* key1, const void* key2){
return *(const int*)key1 > *(const int*)key2 ? - :
*(const int*)key1 < *(const int*)key2 ? : ;
} /**
打印int型数组
*/
void print_arr(const void* arr, int size)
{
const int* pint = (const int*)arr;
int i;
for(i = ; i < size; i++)
{
printf("%d ", pint[i]);
}
printf("\n");
}
归并排序 & 计数排序 & 基数排序 & 冒泡排序 & 选择排序 ----> 内部排序性能比较的更多相关文章
- 【PHP数据结构】其它排序:简单选择、桶排序
这是我们算法正式文章系列的最后一篇文章了,关于排序的知识我们学习了很多,包括常见的冒泡和快排,也学习过了不太常见的简单插入和希尔排序.既然今天这是最后一篇文章,也是排序相关的最后一篇,那我们就来轻松一 ...
- 排序算法练习--JAVA(:内部排序:插入、选择、冒泡、快速排序)
排序算法是数据结构中的经典算法知识点,也是笔试面试中经常考察的问题,平常学的不扎实笔试时候容易出洋相,回来恶补,尤其是碰到递归很可能被问到怎么用非递归实现... 内部排序: 插入排序:直接插入排序 选 ...
- php基础排序算法 冒泡排序 选择排序 插入排序 归并排序 快速排序
<?php$arr=array(12,25,56,1,75,13,58,99,22);//冒泡排序function sortnum($arr){ $num=count($arr); ...
- 9, java数据结构和算法: 直接插入排序, 希尔排序, 简单选择排序, 堆排序, 冒泡排序,快速排序, 归并排序, 基数排序的分析和代码实现
内部排序: 就是使用内存空间来排序 外部排序: 就是数据量很大,需要借助外部存储(文件)来排序. 直接上代码: package com.lvcai; public class Sort { publi ...
- 牛客网Java刷题知识点之插入排序(直接插入排序和希尔排序)、选择排序(直接选择排序和堆排序)、冒泡排序、快速排序、归并排序和基数排序(博主推荐)
不多说,直接上干货! 插入排序包括直接插入排序.希尔排序. 1.直接插入排序: 如何写成代码: 首先设定插入次数,即循环次数,for(int i=1;i<length;i++),1个数的那次不用 ...
- C# 插入排序 冒泡排序 选择排序 高速排序 堆排序 归并排序 基数排序 希尔排序
C# 插入排序 冒泡排序 选择排序 高速排序 堆排序 归并排序 基数排序 希尔排序 以下列出了数据结构与算法的八种基本排序:插入排序 冒泡排序 选择排序 高速排序 堆排序 归并排序 基数排序 希尔排序 ...
- Python八大算法的实现,插入排序、希尔排序、冒泡排序、快速排序、直接选择排序、堆排序、归并排序、基数排序。
Python八大算法的实现,插入排序.希尔排序.冒泡排序.快速排序.直接选择排序.堆排序.归并排序.基数排序. 1.插入排序 描述 插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得 ...
- Python实现八大排序(基数排序、归并排序、堆排序、简单选择排序、直接插入排序、希尔排序、快速排序、冒泡排序)
目录 八大排序 基数排序 归并排序 堆排序 简单选择排序 直接插入排序 希尔排序 快速排序 冒泡排序 时间测试 八大排序 大概了解了一下八大排序,发现排序方法的难易程度相差很多,相应的,他们计算同一列 ...
- 七内部排序算法汇总(插入排序、Shell排序、冒泡排序、请选择类别、、高速分拣合并排序、堆排序)
写在前面: 排序是计算机程序设计中的一种重要操作,它的功能是将一个数据元素的随意序列,又一次排列成一个按keyword有序的序列.因此排序掌握各种排序算法很重要. 对以下介绍的各个排序,我们假定全部排 ...
随机推荐
- RGB颜色空间与YCbCr颜色空间的互转
在人脸检测中会用到YCbCr颜色空间,因此就要进行RGB与YCbCr颜色空间的转换.在下面的公式中RGB和YCbCr各分量的值的范围均为0-255. RGB转到YCbCr: float y= (col ...
- Java 并发编程实战 摘要
第一部分小结 并发技巧清单: 可变状态是至关重要的(It's the mutable state ,stupid). 所有的并发问题结为如何协调对并发状态的访问,可变状态越少,就越容易确保线程安全性. ...
- 添加 SecondaryNameNode
网络上的很多人写的过程都是错的,关键配置反而不写. SecondaryNameNode的启动有两种方式 一:在整个hdfs系统启动时,在namenode上执行start-dfs.sh则namenode ...
- VMware NAT模式 Cent OS IP配置
1:首先VMware 桥接模式 CentOS ip 配置,关键点,ip的网关和DNS1设置成宿主机的网关和DNS 原理:桥接的模式就是通过物理网卡实现的. 2:以图展示VMware NAT模式 Cen ...
- php5.3 连接 sqlserver2005
操作系统:XP php5.3以后,已经不对sqlserver支持连接扩展了,不过微软官方还是对php5.3以后进行了扩展解决方案. 1.确认要连接sqlserver的数据库版本为2005 2.确认ph ...
- Mysql 自定义随机字符串
前几天在开发一个系统,需要用到随机字符串,但是mysql的库函数有没有直接提供,就简单的利用现有的函数东拼西凑出随机字符串来.下面简单的说下实现当时. 1.简单粗暴. select ..., subs ...
- 使用BootStrap制作用户登录UI
先看看劳动成果 布局 左右各一半(col-md-6) 左侧登录框占左侧一半的10/12 右侧是登录系统的注意事项 使用到的BootStrap元素 well 输入框组(input-group) 按钮(b ...
- 那么如何添加网站favicon.ico图标
1. 获得一个favicon.ico的图标,大小为16px×16px最为合适 2. 将制作好的图标文件Favicon.ico上传到网站的根目录: 3. 在首页文件的html代码的头部中加入如下代码: ...
- js设计模式(12)---职责链模式
0.前言 老实讲,看设计模式真得很痛苦,一则阅读过的代码太少:二则从来或者从没意识到使用过这些东西.所以我采用了看书(<js设计模式>)和阅读博客(大叔.alloyteam.聂微东)相结合 ...
- C# 运行时编辑 节点重命名
方法一: ; bool nodeChanged = false; //右键点击,就进入修改状态 private void treeView1_NodeMouseClick(object sender, ...