四大系统级编程语言对比(语言层面)

本文进行四大系统级编程语言在语言层面的对比。发现本文勘误、不合理之处,可以进行指出。目前博客暂未启动评论区。

本文进行对比时,均采用当时最新版本的语言。

本文的大致框架:

  1. 语言特性的对比。这部分包括语言的类型系统、内存管理、编程范式的对比。
  2. 语言语法的对比。这部分包括语言的类型名称、类型声明方式、各种语句和语句块的写法的对比。
  3. 程序结构化对比。这部分包括在语言的函数定义、结构(类)定义、枚举定义的写法的对比。
  4. 数据共享的对比。这部分包括语言如何进行数据的共享(不复制)。
  5. 程序设计的对比。这部分位于文章结尾,使用四种语言编写了一个最小生成树的算法,均使用工程代码规范。如果你想要快速地、全局地了解语言差异,优先阅读此部分。

四大系统级编程语言

本文所指的四大系统级编程语言分别是:C、C++、Go、Rust。这是目前公认的四种,即便 Go 语言存在一定争议。由于 Go 语言在性能上可以追上 C 与 C++,而语法上又大部分借鉴于 C 语言,使用方法也类似,也不止可以用在高并发网络领域,所以我们把 Go 也算作其中一员。

关于本文的定义

为了更好地对编程语言进行分类,本文中使用了一些基于又高于学界工人的分类标准和定义。我们会给出本文使用的分类标准的定义。

语言特性对比

类型系统

定义:

  • 类型动态性:表示变量和数据的类型是否运行时可变。这是相对 JavaScript、Python 等语言而言的,并不是指面向对象中的多态的可变性。

    • 静态类型:表示不可变。
    • 动态类型:表示可变。
  • 类型严格性:表示允许的隐式类型转换与不同类型操作的程度。

    • 弱类型:表示默认就进行隐式类型转换,如 JavaScript 语言允许将一个字符串与一个数字进行操作、== 符号会进行很多类型转换。
    • 强类型:表示不允许不安全的隐式类型转换,但允许合理的隐式类型转换,如数值类型的互相转换等。
    • 严格类型:表示不允许几乎所有的隐式类型转换,哪怕是数值的转换等,强制要求类型匹配。
  • 共享数据类型:表示使用哪种方式的数据类型进行数据共享。仅从语言原始层面,不考虑标准库。

    • 基于指针:使用指针(底层是内存地址)进行数据共享。指针不拥有数据,但不要求数据存在。
    • 基于引用:使用引用进行数据共享。多个引用可以同时“拥有”一个数据。
    • 基于借用(所有权):使用借用进行数据共享。一个数据只能有一个所有者,借用必须满足借用条件。
语言 类型动态性 类型严格性 共享数据类型
C 静态类型 强类型 基于指针
C++ 静态类型 强类型 基于指针
Go 静态类型 严格类型 基于引用
Rust 静态类型 严格类型 基于借用(所有权)

提示

Go 语言中有指针类型,但其指针类型行为与 Java 的引用类似,使用垃圾回收机制,所以归为基于引用的类型系统。

内存管理

定义:

  • 手动内存管理:手动进行内存的分配和释放。内存安全由程序员全权负责。

  • 智能指针管理:使用标准库提供的智能指针,将指针的内存管理自动化。程序员需要承担一定的内存安全责任。

  • 垃圾回收自动内存管理:基于垃圾回收的自动内存管理,程序仅分配,后台检查内存使用情况并自动释放。程序员一般不需要承担内存安全的责任。

  • 生命周期自动内存管理:基于静态的生命周期分析,编译时决定数据分配和释放的位置。程序员需要管理生命周期,但编译器承担内存安全的责任。

对于智能指针管理和垃圾回收自动内存管理的区别,前者不需要运行时或者后台暂停,而后者需要。

语言 内存管理形式
C 手动内存管理
C++ 手动内存管理
Go 垃圾回收自动内存管理
Rust 生命周期自动内存管理

提示

C++ 有 std::shared_ptrstd::unique_ptr 等类型,它们是智能指针。但是它们是基于标准库实现的包装类型,只是将手动内存管理的任务包装起来了而已。所以在语言层面我们不认为 C++ 有智能指针管理。

Rust 语言中有 RcArc 类型,它们也是智能指针。但它没有打破 Rust 的“一个数据只能有一个所有者”的限制,它仅仅包装了一个共享的数据,对于内部的这个数据来说,类似于多所有者。Rust 将 RcArc 类型视作普通类型,一视同仁地进行生命周期分析,并决定分配和释放的位置,以及检查借用合法性。所以此处不认为 Rust 语言层面有智能指针管理。

编程范式

定义:

  • 过程式:与传统定义一致,强调代码的模块化,用函数或子过程分解问题。

  • 函数式:与传统定义一致,但没有那么严苛的要求,不一定需要纯函数式的内容,如不可变变量。

  • 面向对象:与传统定义基本一致,有多态、继承等概念。

  • 行为抽象:将某些类型的共同行为抽象出来,并且让这些类型实现该抽象。有些地方将此范式归入面向对象的范畴,但是两者在类型系统实现和编码习惯上有本质区别,而不仅仅是行为抽象没有继承等表面上的区别。

  • 泛型编程:与传统定义基本一致,在编写代码时不明确指定类型,在使用时以类似参数的形式确定类型。

  • 声明式:与命令式相对,描述目标性质和功能而非下达具体指令和步骤。如 SQL 语言。此类语言不一定要图灵完备。

  • 逻辑式:以数学逻辑表达式为基础构建程序,程序环境进行逻辑分析推理。如 Prolog 语言。

  • 标记式:以特定标记完成特定功能。如 HTML、Markdown、LaTeX 等。此类语言不一定要图灵完备,但也有完备者,如新兴排版编程语言 Typst。

  • 元编程:能够操作程序自身或其他程序结构,在编译时或运行时动态地进行修改代码,最常见的是反射机制。

  • 命令式:使用一系列明确的命令(或指令)排列组合构建程序,主要以改变程序状态进行编程。

语言 编程范式(第一位加粗为主要范式,后无先后顺序)
C 过程式
命令式
C++ 面向对象
行为抽象:抽象类与纯抽象类
过程式
函数式:标准库提供
元编程:模板元编程和标准库提供的有限的运行时多类型
泛型编程:模板元编程
命令式
Go 行为抽象:使用接口 interface
过程式
函数式:部分
元编程:运行时反射,使用 reflect
泛型编程:后期引入类型参数
命令式
Rust 行为抽象:使用特征 trait
过程式
函数式:极其强大的迭代器
元编程:编译时抽象语法树(AST)的修改,使用过程宏和属性宏
泛型编程
命令式

语言语法对比

数据类型对比

类型 C C++ Go Rust
平台特定整数 intunsigned int intuint usizeisize
8 位整数 int8_tuint8_t std::int8_tstd::uint8_t int8uint8 i8u8
16 位整数 int16_tuint16_t std::int16_tstd::uint16_t int16uint16 i16u16
32 位整数 int32_tuint32_t std::int32_tstd::uint32_t int32uint32 i32u32
64 位整数 int64_tuint64_t std::int64_tstd::uint64_t int64uint64 i64u64
128 位整数 标准内无 i128u128
单精度浮点型 float float32 f32
双精度浮点型 double float64 f64
长双精度浮点型(硬件依赖) long double 标准内无
布尔型 bool
单精度复数 float _Complex std::complex<float> complex64 std::complex::Complex<f32>
双精度复数 double _Complex std::complex<double> complex128 std::complex::Complex<f64>
高精度整数 标准内无,需手动实现或第三方库 内置 big.Int 第三方库 num_bigint::BigInt
高精度浮点数 标准内无,需手动实现或第三方库 内置 big.Float 第三方库 decimal::d128
分数 / 有理数 标准内无,需手动实现 第三方库 boost::rational<int_type> 标准库扩展 big.Rat 第三方库 num_rational::Ratio
空类型 / 无返回类型 void 无显式,可用空结构 struct{} ()!
共享数据类型(所有) 指针 type* 指针 type* 与引用 type& 类指针 *type 借用 &type&mut type
字符类型 char 单字节 / UChar32 ICU 第三方库提供的 Unicode 字符 char 遗留单字节 / wchar_t 不推荐使用的宽字符 / char8_t UTF-8 / char16_t UTF-16 / char32_t UTF-32 或 Unicode 字符 byte 单字节 / rune Unicode 字符 u8 单字节 / char Unicode 字符

静态数组对比

仅展示类型声明形式,不考虑变量等问题。

C

1
int array[100];

C++

1
2
int origin[100];  // 原始
std::array<int, 100> modern; // 现代

Go

1
var array [100]int

Rust

1
let array = [i32; 100];

动态数组对比

仅展示类型声明形式,不考虑变量等问题。

C

手动实现。使用指针管理内存:

1
int* array;

动态分配和释放内存:

1
2
3
4
5
int* array = malloc(3 * sizeof(int));  // 分配
array[0] = 1;
array[1] = 2;
array[2] = 3;
free(array); // 释放

C++

1
2
std::vector<int> array{ 1, 2, 3 };  // 初始化
std::vector<int> arr_with_len(10); // 带长度

Go

1
2
array := []int{ 1, 2, 3 }  // 初始化
arrWithLen := make([]int, 10) // 带长度

Rust

1
2
3
// 加了显示类型声明
let array: Vec<i32> = vec![1, 2, 3]; // 初始化
let arr_with_len: Vec<_> = vec![0; 10]; // 带长度,自动类型推导

字符串对比

C

1
2
3
const char* str1 = "Hello!";  // 字面量
char* str2 = malloc(7 * sizeof(char)); // 动态
// 依靠第三方库 ICU 可以(很复杂地)实现 Unicode 字符串

C++

1
2
3
4
5
const char* str1 = "Hello!";  // 字面量
std::string str2; // 现代,可表示 UTF-8
std::u16string str3; // UTF-16
std::u32string str4; // UTF-32
icu::UnicodeString str5; // 第三方库 ICU

Go

1
2
str1 := "Hello!"  // 字面量
var str2 string // 显式声明类型,UTF-8

Rust

1
2
let str1: &str = "Hello!";  // 字面量
let str2: String; // 动态,强制 UTF-8

哈希表对比

C

手动实现。

C++

1
2
3
4
5
6
std::unordered_map<std::string, int> table = {
{ "Perfect": 100 },
{ "Excellent": 80},
{ "Pass": 60 },
{ "Fail": 0 },
};

Go

1
2
3
4
5
6
table := map[string]int{
"Perfect": 100,
"Excellent": 80,
"Pass": 60,
"Fail": 0,
}

Rust

1
2
3
4
5
let mut table = HashMap::new();
table.insert("Perfect", 100);
table.insert("Excellent", 80);
table.insert("Pass", 60);
table.insert("Fail", 0);

控制流对比

条件判断

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (cond) {
// ...
}

if (cond) {
// ...
} else {
// ...
}

if (cond1) {
// ...
} else if (cond2) {
// ...
} else if (cond3) {
// ...
} else {
// ...
}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (cond) {
// ...
}

if (cond) {
// ...
} else {
// ...
}

if (cond1) {
// ...
} else if (cond2) {
// ...
} else if (cond3) {
// ...
} else {
// ...
}

if (int num = 1; cond) { // 带初始临时变量
// ...
} // else-if else

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if cond {
// ...
}

if cond {
// ...
} else {
// ...
}

if cond1 {
// ...
} else if cond2 {
// ...
} else if cond3 {
// ...
} else {
// ...
}

Rust

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if cond {
// ...
}

if cond {
// ...
} else {
// ...
}

if cond1 {
// ...
} else if cond2 {
// ...
} else if cond3 {
// ...
} else {
// ...
}

if let Pattern = something { // 模式匹配判断
// ...
} // else-if else

迭代循环

C

1
2
3
for (int i = 0; i < 100; i++) {
// ...
}

C++

1
2
3
4
5
6
7
for (int i = 0; i < 100; i++) {
// ...
}

for (int i : std::views::iota(0, 100)) {
// ...
}

Go

1
2
3
4
5
6
7
for i := 0; i < 100; i++ {
// ...
}

for i := range [100]struct{} {
// ...
}

Rust

1
2
3
for i in 0..100 {
// ...
}

范围循环

C

无。

C++

1
2
3
for (auto ele : something) {
// ...
}

Go

1
2
3
for _, ele := range something {
// ...
}

Rust

1
2
3
4
5
6
7
for ele in &something {
// ...
}

for ele in something.iter() {
// ...
}

条件循环

C

1
2
3
4
5
6
7
while (cond) {
// ...
}

for (;cond;) {
// ...
}

C++

1
2
3
4
5
6
7
while (cond) {
// ...
}

for (;cond;) {
// ...
}

Go

1
2
3
4
5
6
7
for cond {
// ...
}

for ;cond; {
// ...
}

Rust

1
2
3
4
5
6
7
while cond {
// ...
}

while let Pattern = something { // 模式匹配循环
// ...
}

无限循环

C

1
2
3
4
5
6
7
8
9
10
11
while (true) {
// ...
}

for (;true;) {
// ...
}

for (;;) {
// ...
}

C++

1
2
3
4
5
6
7
8
9
10
11
while (true) {
// ...
}

for (;true;) {
// ...
}

for (;;) {
// ...
}

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for {
// ...
}

for true {
// ...
}

for ;true; {
// ...
}

for ;; {
// ...
}

Rust

1
2
3
4
5
6
7
loop {
// ...
}

while true {
// ...
}

匹配选择

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (value) {
case val1:
// ...
break;
case val2:
// ...
// fallthrough
case val3:
// ...
break;
default:
// ...
break;
}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (value) {
case val1:
// ...
break;
case val2:
// ...
// fallthrough
case val3:
// ...
break;
default:
// ...
break;
}

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch value {
case val1:
// ...
case val2:
// ...
fallthrough
case val3:
// ...
default:
// ...
}

switch {
case cond1: // 布尔条件
// ...
default:
// ...
}

Rust

1
2
3
4
5
6
7
8
9
10
11
12
match value {  // 模式匹配
Pattern1 => /* ... */,
Pattern2 => {
// ...
}
Pattern3 => {
// ...
}
_ => {
// ...
}
}

程序结构化对比

函数

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 无参无返回
void no_arg_no_ret() {
// ...
}
// 旧式
void no_arg_no_ret_old(void) {
// ...
}

// 无参有返回
int no_arg_has_ret() {
return 1;
}
int no_arg_has_ret_old(void) {
return 1;
}

// 有参无返回
void has_arg_no_ret(int a, double b) {
// ...
}

// 有参有返回
int has_arg_has_ret(int a, int b) {
return a + b;
}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 无参无返回
void no_arg_no_ret() {
// ...
}
auto no_arg_no_ret_new() -> void {
// ...
}

// 无参有返回
int no_arg_has_ret() {
return 1;
}
auto no_arg_has_ret_auto() {
return 1;
}
auto no_arg_has_ret_new() -> int {
return 1;
}

// 有参无返回
void has_arg_no_ret(int a, double b) {
// ...
}
auto has_arg_no_ret_new(int a, double b) -> void {
// ...
}

// 有参有返回
int has_arg_has_ret(int a, int b) {
return a + b;
}
auto has_arg_has_ret_auto(int a, int b) {
return a + b;
}
auto has_arg_has_ret_decl(int a, int b) -> decltype(a + b) {
return a + b;
}
auto has_arg_has_ret_new(int a, int b) -> int {
return a + b;
}

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 无参无返回
func NoArgNoRet() {
// ...
}

// 无参有返回
func NoArgHasRet() int {
return 1
}
func NoArgHasRetWithName() ret int {
ret := 1
return
}

// 有参无返回
func HasArgNoRet(a int, b float64) {
// ...
}

// 有参有返回
func HasArgHasRet(a, b int) int {
return a + b
}
func HasArgHasRetWithName(a, b int) sum int {
sum := a + b
return
}
func HasArgHasRetExplicitTypes(a int, b int) int {
return a + b
}

Rust

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 无参无返回
fn no_arg_no_ret() {
// ...
}

// 无参有返回
fn no_arg_has_ret() -> i32 {
1
}
fn no_arg_has_ret_with_return() -> i32 {
return 1;
}

// 有参无返回
fn has_arg_no_ret(a: i32, b: f64) {
// ...
}

// 有参有返回
fn has_arg_has_ret(a: i32, b: i32) -> i32 {
a + b
}
fn has_arg_has_ret_with_return(a: i32, b: i32) -> i32 {
return a + b;
}

结构(类)定义

C

1
2
3
4
5
6
struct Type {
int a;
double b;
char c;
const char* name;
};

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct Type {
// 默认 public
int a;
public:
double b;
protected:
char c;
private:
std::string name;

public:
void method() {
// ...
}

static void static_method() {
// ...
}
};

class TypeClass {
// 默认 private
int a;
public:
double b;
protected:
char c;
private:
std::string name;

public:
void method() {
// ...
}

static void static_method() {
// ...
}
};

Go

1
2
3
4
5
6
7
8
9
10
type Type struct {
a int
b float64
c rune
Name string // public
}

func (this *Type) Method() {
// ...
}

Rust

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Type {
a: i32;
b: f64;
c: char;
pub name: String; // public
}

impl Type {
pub fn method(&self) {
// ...
}

pub fn static_method() {
// ...
}
}

枚举定义

C

1
2
3
4
5
6
7
8
9
enum Day {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
};

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Day {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
};

enum class DayEnum {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
};

Go

1
2
3
4
5
6
7
8
9
const (
Sun = iota
Mon
Tue
Wed
Thu
Fri
Sat
)

Rust

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Day {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat
}

// 枚举变体
enum Message {
Shutdown,
Send(String),
Error {
code: usize,
msg: String,
},
Sleep(usize, usize),
}

数据共享对比

C

指针

1
2
3
4
5
int number = 1;
int* pointer = &number; // 获取指针
int other = *pointer; // 获取指针值
*pointer = 2; // 设置指针值
pointer = &other; // 重设目标

指向不可变指针(常量指针)

1
2
3
4
5
int number = 1;
int* const pointer = &number;
int other = *pointer;
*pointer = 2;
// pointer = &other; --- Error

数据不可变指针(指向常量的指针)

1
2
3
4
5
int number = 1;
const int* pointer = &number;
int other = *pointer;
// *pointer = 2; --- Error
pointer = &other;

指向与数据不可变指针(指向常量的常量指针)

1
2
3
4
5
int number = 1;
const int* const pointer = &number;
int other = *pointer;
// *pointer = 2; --- Error
// pointer = &other; --- Error

C++

指针

与 C 语言完全一致。

引用

1
2
3
4
int number = 1;
int& reference = number; // 创建引用
int other = reference; // 读取引用值
reference = 2; // 设置引用值

不可变引用

1
2
3
4
int number = 1;
const int& reference = number;
int other = reference;
// reference = 2; --- Error

Go

指针

显式定义类型:

1
2
3
4
5
var number int = 1
var pointer *int = &number // 创建指针
var other int = *pointer // 读取指针值
*pointer = 2 // 设置指针值
pointer = &other // 重设目标

简写声明:

1
2
3
4
5
number := 1
pointer := &number
other := *pointer
*pointer = 2
pointer = &other

Rust

可变借用

1
2
3
4
5
let mut number = 1;
let borrow = &mut number; // 创建借用
let mut other = *borrow; // 读取借用值(若实现 Copy 则复制,否则转移所有权)
*borrow = 2; // 设置借用值(若实现 Copy 则复制,否则转移所有权)
// borrow = &mut other; --- 需要 mut 变量才能重设目标

不可变借用

1
2
3
4
let number = 1;
let borrow = &number;
let other = *borrow;
// *borrow = 2; --- Error

程序设计对比

接下来,我们用这四种语言实现同一个程序,来从全局的角度对比一下这四种语言。

程序目标

输入一个无向带权图,该图不一定连通。求出此图的最小生成树的边权之和,如果此图不连通,则输出 Not connected.

从标准输入读取图的信息。第一行两个整 VVEE,表示图的点数与边数。接下来共 MM 行,每行参个整数 aabbww,代表点 aa 与点 bb 之间有一条无向边,边权是 ww

将结果输出到标准输出。如果该图连通,输出最小生成树的边权之和;否则,输出 Not connected.

点的序号 aabb 采用以 1-based 索引(即索引从 1 开始)。

程序设计

我们使用 Kruskal 算法求最小生成树。简单说明一下算法思路:输入所有边后,对边按照边权升序排序,并从最小边权的边开始处理。使用并查集来处理哪些顶点在已知的最小生成树中,哪些不在。每次处理一条边时,需要判断这个边所连接的两个点是否已经都在最小生成树中了,也就是这两个点是否已经在并查集中连通了。只要有一个点不在已知的最小生成树中,我们将此边加入最小生成树,在并查集中连通两个顶点,并累加边权。这是一种典型的贪心算法,时间复杂度为 O(ElogE)O\left(E \log E\right),空间复杂度为 O(E+V)O\left(E + V\right),其中 EE 是边数,VV 是点数,计算机科学中对数的默认底为 22

根据树的性质:边段数量为点的数量减一。所以当我们处理了足够多的边时,停止处理并输出累加边权。

如果最后所有的边处理完成但是已知的最小生成树中还没有足够的边,说明原图不连通。这时我们输出 Not connected.

标准输入的节点是 1-based 索引,为了方便处理,我们在进行数据输入时就将其转换为 0-based 索引。

关于图、树、最小生成树、Kruskal 算法、贪心算法等内容,感兴趣的可以自行搜索更多资料。

代码

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef int32_t int32;
typedef size_t usize;

typedef struct {
int32* father;
} UnionFind;

typedef struct {
int32 a;
int32 b;
int32 weight;
} Edge;

int edge_cmp(const void* a, const void* b);
void read_data(usize* node_cnt, usize* edge_cnt, Edge** const edges);
bool kruskal(const usize node_cnt, const usize edge_cnt, Edge* const edges, int32* const result);
UnionFind* create_union_find(const usize size);
void free_union_find(UnionFind* const self);
int32 find_root(UnionFind* const self, const int32 a);
bool connected(UnionFind* const self, const int32 a, const int32 b);
void merge(UnionFind* const self, const int32 a, const int32 b);

int main() {
usize node_cnt = 0;
usize edge_cnt = 0;
Edge* edges = NULL;

read_data(&node_cnt, &edge_cnt, &edges);

int32 result = -1;
const bool succeeded = kruskal(node_cnt, edge_cnt, edges, &result);

if (succeeded) {
printf("%" PRId32 "\n", result);
} else {
printf("Not connected.\n");
}

free(edges);

return 0;
}

int edge_cmp(const void* a, const void* b) {
const Edge* edge_a = (const Edge*) a;
const Edge* edge_b = (const Edge*) b;
if (edge_a->weight < edge_b->weight) {
return -1;
} else if (edge_a->weight > edge_b->weight) {
return 1;
} else {
return 0;
}
}

void read_data(usize* node_cnt, usize* edge_cnt, Edge** const edges) {
const int input_res = scanf("%zu%zu", node_cnt, edge_cnt);
if (input_res != 2) {
exit(1);
}

*edges = (Edge*) malloc(*edge_cnt * sizeof(Edge));
if (*edges == NULL) {
exit(1);
}

for (usize i = 0; i < *edge_cnt; i++) {
int32 a, b, weight;
const int input_res = scanf("%" PRId32 "%" PRId32 "%" PRId32, &a, &b, &weight);
if (input_res != 3) {
exit(1);
}
(*edges)[i] = (Edge){
.a = a - 1,
.b = b - 1,
.weight = weight,
};
}
}

bool kruskal(const usize node_cnt, const usize edge_cnt, Edge* const edges, int32* const result) {
qsort(edges, edge_cnt, sizeof(Edge), edge_cmp);

UnionFind* const set = create_union_find(node_cnt);

int32 total = 0;
int32 handled = 0;
for (usize i = 0; i < edge_cnt; i++) {
const Edge* const edge = &edges[i];
if (connected(set, edge->a, edge->b)) {
continue;
}
merge(set, edge->a, edge->b);
total += edge->weight;
handled++;
if (handled == (int32) node_cnt - 1) {
*result = total;
free_union_find(set);
return true;
}
}

free_union_find(set);
return false;
}

UnionFind* create_union_find(const usize size) {
UnionFind* const self = (UnionFind* const) malloc(sizeof(UnionFind));
if (self == NULL) {
exit(1);
}

self->father = (int32*) malloc(size * sizeof(int32));
if (self->father == NULL) {
exit(1);
}

for (usize i = 0; i < size; i++) {
self->father[i] = (int32) i;
}

return self;
}

void free_union_find(UnionFind* const self) {
free(self->father);
free(self);
}

int32 find_root(UnionFind* const self, const int32 a) {
if (self->father[a] == a) {
return a;
}
const int32 temp = find_root(self, self->father[a]);
self->father[a] = temp;
return temp;
}

bool connected(UnionFind* const self, const int32 a, const int32 b) {
return find_root(self, a) == find_root(self, b);
}

void merge(UnionFind* const self, const int32 a, const int32 b) {
const int32 root_a = find_root(self, a);
const int32 root_b = find_root(self, b);
self->father[root_a] = root_b;
}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <memory>
#include <optional>
#include <tuple>
#include <vector>

using int32 = std::int32_t;
using usize = std::size_t;

class UnionFind final {
private:
const std::unique_ptr<int32[]> father;
auto find_root(const int32 a) -> int32;

public:
UnionFind(const usize size);
auto connected(const int32 a, const int32 b) -> bool;
void merge(const int32 a, const int32 b);
};

class Edge final {
public:
int32 a;
int32 b;
int32 weight;

Edge(const int32 a, const int32 b, const int32 weight);
};

auto read_data() -> std::tuple<usize, std::vector<Edge>>;
auto kruskal(const usize node_cnt, std::vector<Edge>& edges) -> std::optional<int32>;

auto main() -> int {
auto [node_cnt, edges] = read_data();

const std::optional<int32> result = kruskal(node_cnt, edges);

if (result.has_value()) {
std::cout << result.value() << '\n';
} else {
std::cout << "Not connected." << '\n';
}

return 0;
}

auto read_data() -> std::tuple<usize, std::vector<Edge>> {
usize node_cnt, edge_cnt;
std::cin >> node_cnt >> edge_cnt;

std::vector<Edge> edges;
for (usize i = 0; i < edge_cnt; i++) {
int32 a, b, weight;
std::cin >> a >> b >> weight;
edges.push_back(Edge(a - 1, b - 1, weight));
}

return std::make_tuple(node_cnt, std::move(edges));
}

auto kruskal(const usize node_cnt, std::vector<Edge>& edges) -> std::optional<int32> {
std::ranges::sort(edges, [](const Edge& a, const Edge& b) -> bool {
return a.weight < b.weight;
});

UnionFind set(node_cnt);

int32 total = 0;
int32 handled = 0;
for (const Edge& edge : edges) {
if (set.connected(edge.a, edge.b)) {
continue;
}
set.merge(edge.a, edge.b);
total += edge.weight;
handled++;
if (handled == static_cast<int32>(node_cnt) - 1) {
return std::make_optional(total);
}
}

return std::nullopt;
}

UnionFind::UnionFind(const usize size) : father(std::make_unique<int32[]>(size)) {
for (usize i = 0; i < size; i++) {
father[i] = static_cast<int32>(i);
}
}

auto UnionFind::connected(const int32 a, const int32 b) -> bool {
return find_root(a) == find_root(b);
}

void UnionFind::merge(const int32 a, const int32 b) {
const int32 root_a = find_root(a);
const int32 root_b = find_root(b);
father[root_a] = root_b;
}

auto UnionFind::find_root(const int32 a) -> int32 {
if (father[a] == a) {
return a;
}
const int32 temp = find_root(father[a]);
father[a] = temp;
return temp;
}

Edge::Edge(const int32 a, const int32 b, const int32 weight) : a(a), b(b), weight(weight) {}

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package main

import (
"fmt"
"sort"
)

type UnionFind struct {
father []int
}

type Edge struct {
a int
b int
weight int
}

func main() {
nodeCnt, edges := readData()

result := kruskal(nodeCnt, edges)

if result != nil {
fmt.Println(*result)
} else {
fmt.Println("Not connected.")
}
}

func readData() (nodeCnt int, edges []Edge) {
var edgeCnt int
fmt.Scan(&nodeCnt, &edgeCnt)

edges = make([]Edge, edgeCnt)
for i := range edgeCnt {
var a, b, weight int
fmt.Scan(&a, &b, &weight)
edges[i] = Edge {
a: a - 1,
b: b - 1,
weight: weight,
}
}

return
}

func kruskal(nodeCnt int, edges []Edge) *int {
sort.Slice(edges, func(i, j int) bool {
return edges[i].weight < edges[j].weight
})

set := newUnionFind(nodeCnt)

total := 0
handled := 0
for _, edge := range edges {
if set.connected(edge.a, edge.b) {
continue
}
set.merge(edge.a, edge.b)
total += edge.weight
handled++
if handled == nodeCnt - 1 {
return &total
}
}

return nil
}

func newUnionFind(size int) *UnionFind {
set := UnionFind {
father: make([]int, size),
}
for i := range size {
set.father[i] = i
}
return &set
}

func (set *UnionFind) connected(a, b int) bool {
return set.findRoot(a) == set.findRoot(b)
}

func (set *UnionFind) merge(a, b int) {
rootA := set.findRoot(a)
rootB := set.findRoot(b)
set.father[rootA] = rootB
}

func (set *UnionFind) findRoot(a int) int {
if set.father[a] == a {
return a
}
temp := set.findRoot(set.father[a])
set.father[a] = temp
return temp
}

Rust

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
struct UnionFind {
father: Box<[i32]>,
}

struct Edge {
a: i32,
b: i32,
weight: i32,
}

fn main() {
let (node_cnt, mut edges) = read_data(&std::io::stdin());

let result = kruskal(node_cnt, &mut edges);

if let Some(res) = result {
println!("{res}");
} else {
println!("Not connected.");
}
}

fn read_data(reader: &std::io::Stdin) -> (usize, Vec<Edge>) {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
let mut iter = line.trim().split(' ').map(|x| x.parse::<usize>().unwrap());
let node_cnt = iter.next().unwrap();
let edge_cnt = iter.next().unwrap();

let mut edges = vec![];
for _i in 0..edge_cnt {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
let mut iter = line.trim().split(' ').map(|x| x.parse::<i32>().unwrap());
let a = iter.next().unwrap();
let b = iter.next().unwrap();
let weight = iter.next().unwrap();
edges.push(Edge {
a,
b,
weight,
});
}

(node_cnt, edges)
}

fn kruskal(node_cnt: usize, edges: &mut [Edge]) -> Option<i32> {
edges.sort_by_key(|x| x.weight);

let mut set = UnionFind::new(node_cnt);

let mut total = 0;
let mut handled = 0;
for edge in edges {
if set.connected(edge.a, edge.b) {
continue;
}
set.merge(edge.a, edge.b);
total += edge.weight;
handled += 1;
if handled == (node_cnt as i32) - 1 {
return Some(total);
}
}

None
}

impl UnionFind {
fn new(size: usize) -> Self {
let mut this = UnionFind {
father: vec![0; size].into_boxed_slice(),
};

for i in 0..size {
this.father[i] = i as i32;
}

this
}

fn connected(&mut self, a: i32, b: i32) -> bool {
self.find_root(a) == self.find_root(b)
}

fn merge(&mut self, a: i32, b: i32) {
let root_a = self.find_root(a);
let root_b = self.find_root(b);
self.father[root_a as usize] = root_b;
}

fn find_root(&mut self, a: i32) -> i32 {
if self.father[a as usize] == a {
a
} else {
let temp = self.find_root(self.father[a as usize]);
self.father[a as usize] = temp;
temp
}
}
}