データ構造とアルゴリズム-Rust版読書メモ-2 線形データ構造-スタック
1. 線形データ構造の概念
配列、スタック、キュー、両端キュー、リンク リストなどのデータ構造はすべて、データを格納するためのコンテナです。データ項目間の順序は、追加または削除された順序によって決まります。データ項目が追加されると、前後の要素に応じて変化し、位置を一定に保つデータ構造を線形データ構造と呼びます。
線形データ構造には両端があり、「左」と「右」と呼ばれます。場合によっては、「前」と「後」とも呼ばれます。もちろん、上と下と呼ぶこともできます。名前は重要ではありません。重要なのは、この名前付けによって表示される位置関係は、データが直線的に構成されていることを示しているということです。メモリは一種の線形ハードウェアであるため、この線形特性はメモリと密接に関係しており、ソフトウェアとハードウェアがどのように関係しているかもわかります。
線形データ構造は、データの保存方法ではなく、データへのアクセス方法に関係します。
線形データ構造は、必ずしもデータ項目がメモリ内で隣接していることを意味するわけではありません。リンクされたリストを例にとると、データ項目はメモリ内のさまざまな場所にある可能性がありますが、アクセスは直線的です。
異なる線形データ構造を区別する 1 つの方法は、データ項目がどのように追加および削除されるか、特にデータ項目がどこで追加および削除されるかを調べることです。たとえば、データ構造によっては、一方の端からのみ項目を追加できるもの、他方の端から項目を削除できるもの、さらに両端から項目を操作できるものもあります。これらのバリアントとそれらを組み合わせた形式は、コンピューター サイエンスの分野で非常に役立つ多くのデータ構造を生み出し、さまざまな実用的で重要なタスクを実行するためにさまざまなアルゴリズムに現れます。
1. スタック: 後入れ先出し
スタックはデータ項目の順序付けされたコレクションであり、新しい項目の追加と削除は常に同じ端 (上部と呼ばれる) で発生し、反対側の端は下部と呼ばれます。スタックの一番下にあるアイテムは最も長く保存され、最後に追加されたアイテムが最初に削除されるため、スタックの一番下は重要です。この並べ替え原則は、後入れ先出し (LIFO) または先入れ後出し (FILO) と呼ばれることもあり、新しいアイテムが一番上に近くなり、古いアイテムが一番下に近くなります。
2. Rustの予備知識
1、特性
trait
は、Java
のインターフェイス、TS のインターフェイス、および C++
の純粋な仮想クラスに似ていますが、まったく同じではありません。
trait
本来は特徴を意味する言葉で、コード上での意味は「ある構造にある特徴を持たせる」という意味です。
trait Shape {
fn area(&self) -> f32{
return 0.0;
} //该函数是实现可写可不写,如果不写,那么实现该Trait的结构就必须写,如果这里写了,那么后面实现该trait的结构就可以不写
fn test(){
println!("不写self参数,则只能通过 :: 的方式进行调用");
}
}
struct triangle{
//为了简单,假设其是直角三角形,存放两个直角边
a: f32,
b: f32,
}
impl Shape for triangle {
fn area(&self) -> f32 {
return (self.a*self.b)/2.0;
}
}
struct square{
a: f32
}
impl Shape for square {
fn area(&self) -> f32 {
return self.a*self.a;
}
}
特性 Shape により、三角形と正方形の両方に面積メソッドがあります。
呼び出し方法:
fn main() {
let t=triangle{
a: 1.0, b: 2.0};
let s=square{
a:4.0};
//调用带有self参数的函数
t.area();
s.area();
//调用没有self参数的函数
triangle::test();
square::test();
}
エリア関数のパラメータには self
が含まれています。これは、パラメータが特定の構造に対応している必要があることを意味します。呼び出すときは .
を使用します。
別の呼び出し方法:
fn main() {
let t=triangle{
a: 1.0, b: 2.0};
let s=square{
a:4.0};
//调用带有self参数的函数
test_area(&t);
test_area(&s);
}
fn test_area(shape: &impl Shape){
shape.area();
}
test_area 関数を使用して出力します。この関数のパラメーターは &impl Shape であり、これは、この Shape 特性を実装する構造体への参照を受け入れることを意味します。
これはprintln!
マクロに似ていますね。任意の形状構造が Shape
のこの特性を実装している限り、統一メソッド (test_area) を使用してコンテンツを出力できます。
それで:
構造をカスタマイズし、println!
印刷可能にしたい場合は、これを実装する必要がありますtrait
コピーしたい場合は、Clone
thistrait
を実装し、関数を明示的に呼び出してくださいclone
。この時点でデータのコピー作業を完了していることを明確に認識してください
#[derive(Clone)]
struct Stu{
name: String,
age:u32
}
fn main() {
let s1=Stu{
name:String::from("yushi-"),
age:100
};
let s2=s1.clone(); //让你能清醒的认识到自己在完成一个拷贝的工作
println!("{}:{}", s1.name,s1.age); //可用,因为是将内容拷贝给了s2一份
println!("{}:{}", s2.name,s2.age);
}
最常用的trait
,除了Copy
与Clone
,还有三个:Debug
、Default
、PartialEq
その内、Debug
はデバッグに便利です。
#[derive(Debug)]
struct Stu{
name: String,
age:u32
}
fn main() {
let s1=Stu{
/*省略代码*/};
println!("{:?}", s1);
}
Debug
thistrait
を使用する限り、Display
トレイトを実装する必要はありません。関連情報を印刷することも便利です
注意する必要がある唯一のことは、Debug
情報を印刷するには、{}
に を追加する必要があることです。:?
構造を見やすくするために、フォーマットされたフォーマット情報も出力したい場合は、次のように記述することもできます。
println!("{:#?}", s1);
2、アイテム
Vec は、実行時に自動的にサイズが変更される動的配列です。
Vec は Rust 標準ライブラリの一部であり、大量のデータを効率的かつ安全に処理する方法を提供します。
ヒープ メモリ アプリケーションに基づく連続動的データ型の場合、インデックス作成、プッシュ、およびポップ操作の時間計算量は O(1) です。
Vecはベクターの略称です。
Vec の基盤となる実装は配列に基づいているため、そのパフォーマンスは非常に高くなります。 Vec は、整数、浮動小数点数、文字列など、あらゆる種類のデータを保存できます。
Vec は実際には、ヒープ上にメモリの動的配列を割り当てるために使用されるスマート ポインターです。要素の追加、削除、アクセスなど、配列を操作するメソッドを提供します。 C や Python の配列とは異なり、Vec はメモリの割り当てと割り当て解除を自動的に処理し、一般的なメモリ リークやダングリング ポインタ エラーを回避します。
Vec の本質はトリプレット、ポインタ、長さ、容量であり、Rust 標準ライブラリでの定義は次のとおりです。
pub struct Vec<T, A: Allocator = Global> {
buf: RawVec<T, A>,
len: usize,
}
impl<T> Vec<T> {
#[inline]
pub const fn new() -> Self {
Vec {
buf: RawVec::NEW, len: 0 }
}
//...略...
}
Vec の中核的な機能の 1 つは、動的な成長と縮小です。 Vec に要素を追加するときに、ヒープ上のメモリが不十分な場合、Vec は要素を収容するために自動的に追加のメモリを割り当てます。このプロセスは「スケーリング」と呼ばれます。同様に、Vec から要素が削除されるときに、ヒープ上のメモリが多すぎる場合、Vec はメモリを解放するために自動的に縮小されます。このプロセスは「ダウンサイジング」と呼ばれます。この自動メモリ管理メカニズムにより、Vec の使用が非常に便利になり、手動メモリ管理の間違いを回避できます。
要素の追加、削除、アクセスという基本操作に加えて、Vec は他の多くの機能を提供します。たとえば、インデックスによって要素にアクセスしたり、反復子を使用して要素を走査したり、Vec の内容を変更するための複数のメソッド (push()、pop()、insert()、remove() など) をサポートしたりできます。 Vec は、Vec の属性を取得するために使用できるいくつかの便利な静的メソッド (capacity()、len()、is_empty() など) も提供します。
Vecs は非常に強力なデータ構造ですが、いくつかの制限もあります。たとえば、Vec はヒープ上にメモリを割り当てます。これは、スタック上にメモリを割り当てる配列よりも要素へのアクセスが遅くなる可能性があることを意味します。さらに、Vec はスマート ポインターであるため、そのサイズは固定されておらず、プログラミング エラーが発生する可能性があります。たとえば、Vec を固定サイズの配列または別の Vec に割り当てようとすると、コンパイル時エラーが発生します。
Vec::new() メソッド
空のリストのみを作成する場合は、型を指定する必要があります (そうでないとコンパイルに合格しません)。
fn main() {
let vec: Vec<i32> = Vec::new();
println!("{:?}", vec);
}
Vec::from() メソッド
let vec = Vec::from([1,2,3]);
ベック!マクロ
それらが等しいかどうかを判断するために使用されます
fn main() {
let vec1 = Vec::from([1,2,3]);
println!("{:?}", vec1);
let vec2 = vec![1,2,3];
println!("{:?}", vec2);
assert_eq!(vec1, vec2);
assert_eq!(vec1, [1,2,3]);
assert_eq!(vec2, [1,2,3]);
println!("{}", vec1 == vec2); // 输出 true
}
同じ要素 n の vec を作成します
fn main() {
let vec = vec![0; 5];
assert_eq!(vec, [0, 0, 0, 0, 0]);
println!("{:?}", vec);
let vec = vec![1; 3];
assert_eq!(vec, [1, 1, 1]);
println!("{:?}", vec);
let vec = vec![1; 0];
}
配列なので、配列が持つポップ、スプライス、ソートなどのメソッドもあります。
3、インプル
**impl
は、型のメソッドを実装するために使用されるキーワードです。これは、関数を特定の型 (構造体または列挙) に関連付ける方法です。 impl
**主な用途は 2 つあります:
1. 実装方法: 特定のタイプのメソッドを定義できます。これらのメソッドは、その型のインスタンスで呼び出すことができます。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
この例では、** という名前のメソッドが **Rectangle
構造体に実装され、長方形の面積を計算します。 area
2. トレイトの実装: Rust のトレイトは、他の言語のインターフェイスに似ています。これらは、型が提供する必要がある機能を定義します。 **impl
** を使用すると、特定のタイプのトレイトを実装し、トレイトで定義されている必要なメソッドを提供できます。
trait Describable {
fn describe(&self) -> String;
}
impl Describable for Rectangle {
fn describe(&self) -> String {
format!("Rectangle of width {} and height {}", self.width, self.height)
}
}
ここで、**Rectangle
は Describable
** トレイトを実装し、四角形を記述する特定の方法を提供します。
impl
ブロックで定義された関数は独立させることができます。つまり、 Foo::bar()
を呼び出すことができます。関数が最初の引数として self
、&self
、または &mut self
を取る場合、メソッド呼び出し構文を使用して関数を呼び出すこともできます。 foo.bar ()
など、オブジェクト指向プログラマなら誰でもよく知っている機能です。
4、自己
Self は一般に、Rust のトレイトおよび関連関数で、トレイトを実装する型、または関連する関数を呼び出す型を参照するために使用されます。
struct Point {
x: f32,
y: f32,
}
impl Point {
//关联函数
fn origin() -> Self {
Point {
x: 0.0, y: 0.0 }
}
}
fn main() {
let p = Point::origin();
}
5、自分自身
self は型インスタンス (または型の参照や値) を表すキーワードで、Rust メソッドで self を使用すると、現在の型のインスタンスまたは型自体を参照できます。
具体的には、メソッドを定義するときに、メソッドの最初のパラメータとして self キーワードを使用すると、メソッド呼び出し時に型インスタンス自体に直接アクセスできます。
struct Point {
x: f32,
y: f32,
}
impl Point {
fn distance(&self, other: &Point) -> f32 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
6. . と ::
Rust では、.
演算子と ::
演算子の両方を使用してメソッドを呼び出すことができますが、その使用法は異なります。
.
演算子は、インスタンス メソッドを呼び出すために使用されます。インスタンス メソッドは、最初の引数として型のインスタンスを必要とする型に対して定義されたメソッドです (多くの場合、self
と呼ばれます)。 **インスタンス メソッドは、他の言語の動的メソッドに似ています。すべてのメソッドは、使用する前に最初に宣言する必要があります。 **たとえば、単純な構造体とインスタンス メソッドの例を次に示します。
上記のコードは、Point
という名前の構造体を定義します。この構造体には、 x
と y
という 2 つのフィールドがあります。次に、 impl Point
ブロックで distance_from_origin
という名前のインスタンス メソッドを定義します。このメソッドは、メソッドが呼び出されるインスタンスを表す self
という名前のパラメータを受け取ります。このメソッドでは、 self.x
と self.y
を使用してインスタンスのフィールドにアクセスします。
main
関数では、p
という名前の Point
のインスタンスを作成し、 ステートメントを使用してこのメソッドを呼び出します。 .
演算子を使用して、そのインスタンス メソッドを呼び出します。つまり、 p.distance_from_origin()
演算子と ::
演算子は、関連する関数を呼び出すために使用されます。 **関連関数も型に対して定義された関数ですが、最初のパラメーターとして型のインスタンスを必要としません。 Rust の関連関数は、他の言語の静的メソッドに似ています。 **たとえば、単純な構造と関連する関数の例を次に示します。
上記のコードは、Point
という名前の構造体を定義します。この構造体には、 x
と y
という 2 つのフィールドがあります。次に、 impl Point
ブロックで new
という名前の相関関数を定義します。この関数は 2 つのパラメータ x と y を受け取り、新しく作成された Point インスタンスを返します。
main 関数では、:: 演算子を使用して、Point タイプの関連関数を呼び出します。つまり、ステートメント Point::new(3, 4) を使用して関数を呼び出します。
インスタンス メソッドは通常、型のインスタンスを操作するために使用されます。たとえば、 2 つのフィールド と を含む Point
構造体を定義し、からの距離を計算するインスタンス メソッドを定義できます。原点へのポイント。このメソッドは、最初のパラメータとして 型のインスタンスを必要とし、このインスタンスのフィールドを使用して計算を実行します。 x
y
Point
連想関数は通常、型に関連するが、型のインスタンスには依存しない操作を実行するために使用されます。たとえば、関連付けられた関数を定義して、新しいPoint
インスタンスを作成できます。この関数は、最初のパラメータとして Point
型のインスタンスを必要としませんが、代わりにいくつかのパラメータを受け入れて、新しく作成されたインスタンスを初期化します。
インスタンス メソッドを使用するか、関連関数を使用するかを選択する場合は、実行する操作が型のインスタンスに依存するかどうかを考慮する必要があります。その場合はインスタンス メソッドを使用する必要があり、それ以外の場合は関連する関数を使用する必要があります。
7. self と &self、mut と &mut
&self は、参照が関数に渡され、オブジェクトの所有権の譲渡が行われないことを意味します。
self は、オブジェクトが関数に渡され、所有権が転送され、オブジェクトの所有権が関数に渡されることを意味します。
let b = a;
意味: a によってバインドされているリソース A は b に転送され、b はこのリソース A を所有します
let b = &a;
意味: a によってバインドされているリソース A は b に貸し出されており、b はリソース A の読み取り権限のみを持っています
let b = &mut a;
意味: a によってバインドされているリソース A は b に貸し出されており、b はリソース A の読み取りおよび書き込み権限を持っています
let mut b = &mut a;
意味: a によってバインドされているリソース A は b に貸し出されており、b はリソース A の読み取りおよび書き込み権限を持っています。同時に、 b を新しいリソースにバインドできます (バインディングを更新する機能)
fn do(c: String) {}
意味: パラメータを渡すと、実際のパラメータ d によってバインドされたリソース D の所有権が c に転送されます。
fn do(c: &String) {}
意味: パラメータを渡すとき、実際のパラメータ d はバインドされたリソース D を c に貸し出し、c はそのリソースを使用します。 D 読み取り専用
fn do(c: &mut String) {}
意味: パラメータを渡すとき、実際のパラメータ d はバインドされたリソース D を c に貸し出し、c はリソース 恐ろしいほど書き込み可能
fn do(mut c: &mut String) {}
意味: パラメータを渡すとき、実際のパラメータ d はバインドされたリソース D を c に貸し出し、c リソース D は読み書き可能。同時に、 c を新しいリソースにバインドできます (バインディングを更新する機能)
8、オプション
Option<T> は、不正な型を伝播および処理するための Rust の型システムです。
pub enum Option<T> {
None,
Some(T),
}
Option<T>
は列挙型で、Some<T>
または None
のいずれかです。これにより、Java の NullPointerException
を回避して、価値のある状況と価値のない状況の両方を適切に表現できます。
9.' ライフサイクルタグ
ライフサイクルは、一重引用符 ' とその後に続く文字で表され、&'a、&mut 't のように & の後に配置されます。
10、包装を解く
プログラム自体で Err
を処理したくない場合や、OK
の特定の値のみが必要な場合があります。
これら 2 つの Virgo の要求に応えて、Rust 言語の開発者は 2 つのヘルパー関数 unwrap()
と expect()
を標準ライブラリに定義しました。
方法 | プロトタイプ | 説明する |
---|---|---|
包みを解く | unwrap(self):T |
self が Ok または Some の場合、含まれる値を返します。それ以外の場合、マクロ panic!() が呼び出され、プログラムは直ちに終了します。 |
期待する | expect(self,msg:&str):T |
self が Ok または Some の場合、含まれる値を返します。それ以外の場合は、panic!() を呼び出してカスタマイズされたエラーを出力して終了します |
expect()
関数は、失敗したくないエラー状況を簡素化するために使用されます。 unwrap()
関数は、 OK
が成功した場合に返される実際の結果を抽出します。
unwrap()
とexpect()
は、Result <T,E>
列挙を処理できるだけでなく、Option <T>
列挙の処理にも使用できます。列挙。
fn main(){
let result = is_even(10).unwrap();
println!("result is {}",result);
println!("end of main");
}
fn is_even(no:i32)->Result<bool,String> {
if no%2==0 {
return Ok(true);
} else {
return Err("NOT_AN_EVEN".to_string());
}
}
上記の Rust コードをコンパイルして実行すると、出力結果は次のようになります。
thread 'main' panicked at 'called `Result::unwrap()` on
an `Err` value: "NOT_AN_EVEN"', libcore\result.rs:945:5
note: Run with `RUST_BACKTRACE=1` for a backtrace
11.'_ 匿名のライフサイクル
Rust 2018 では、この省略について不明瞭な型について、ライフタイムが省略される場所を明示的にマークできます。これを行うには、構文 で型を明示的にマークするのと同じように、特別な有効期間 '_
を使用できます。 let x:_ = ..;
何らかの理由で、&'a str
には単純なラッパーがあります:
struct StrWrap<'a>(&'a str);
3. Rustコードの実装とスタックの実行結果
スタック.rs
/*
* @Description:
* @Author: tianyw
* @Date: 2023-12-10 17:43:34
* @LastEditTime: 2023-12-10 21:28:31
* @LastEditors: tianyw
*/
#[derive(Debug)] // Debug 是派生宏的名称,此语句为 Stack 结构体实现了 Debug trait
pub struct Stack<T> { // pub 表示公开的
size: usize, // 栈大小
data: Vec<T>, // 栈数据 泛型数组
}
impl<T> Stack<T> { // impl 用于定义类型的实现,如实现 new 方法、is_empty 方法等
// 初始化空栈
pub fn new() -> Self { // 指代 Stack 类型
Self {
size: 0,
data: Vec::new() // 初始化空数组
}
}
pub fn is_empty(&self) -> bool {
0 == self.size // 结尾没有分号,表示返回当前值
}
pub fn len(&self) -> usize { // &self 只可读
self.size // 结尾没有分号 表示返回当前值
}
// 清空栈
pub fn clear(&mut self) { // &mut self 可读、可写
self.size = 0;
self.data.clear();
}
// 将数据保存在 Vec 的末尾
pub fn push(&mut self, val:T) {
self.data.push(val);
self.size +=1;
}
// 在将栈顶减1后,弹出数据
pub fn pop(&mut self) -> Option<T> {
if 0 == self.size { return None; }
self.size -= 1;
self.data.pop()
}
// 返回栈顶数据引用和可变引用
pub fn peek(&self) -> Option<&T> {
if 0 == self.size {
return None;
}
self.data.get(self.size - 1) // 不带分号 获取值并返回
}
pub fn peek_mut(&mut self) -> Option<&mut T> {
if 0 == self.size {
return None;
}
self.data.get_mut(self.size - 1)
}
// 以下是为栈实现的迭代功能
// into_iter:栈改变,成为迭代器
// iter: 栈不变,得到不可变迭代器
// iter_mut: 栈不变,得到可变迭代器
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
pub fn iter(&self) -> Iter<T> {
let mut iterator = Iter { stack: Vec::new() };
for item in self.data.iter() {
iterator.stack.push(item);
}
iterator
}
pub fn iter_mut(&mut self) -> IterMut<T> {
let mut iterator = IterMut { stack: Vec::new() };
for item in self.data.iter_mut() {
iterator.stack.push(item);
}
iterator
}
}
// 实现三种迭代功能
pub struct IntoIter<T>(Stack<T>);
impl<T:Clone> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
if !self.0.is_empty() {
self.0.size -= 1;
self.0.data.pop()
} else {
None
}
}
}
pub struct Iter<'a,T:'a> { stack: Vec<&'a T>, }
impl<'a,T> Iterator for Iter<'a,T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.stack.pop()
}
}
pub struct IterMut<'a,T:'a> { stack: Vec<&'a mut T> }
impl<'a,T> Iterator for IterMut<'a,T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
self.stack.pop()
}
}
main.rs
mod stack;
fn main() {
basic();
peek();
iter();
fn basic() {
let mut s= stack::Stack::new();
s.push(1);
s.push(2);
s.push(3);
println!("size:{},{:?}", s.len(), s);
println!("pop {:?},size {}", s.pop().unwrap(), s.len());
println!("empty: {}, {:?}", s.is_empty(), s);
s.clear();
println!("{:?}", s);
}
fn peek() {
let mut s = stack::Stack::new();
s.push(1);
s.push(2);
s.push(3);
println!("{:?}", s);
let peek_mut = s.peek_mut();
if let Some(top) = peek_mut {
*top = 4;
}
println!("top {:?}", s.peek().unwrap());
println!("{:?}", s);
}
fn iter() {
let mut s = stack::Stack::new();
s.push(1);
s.push(2);
s.push(3);
let sum1 = s.iter().sum::<i32>();
let mut addend = 0;
for item in s.iter_mut() {
*item += 1;
addend += 1;
}
let sum2 = s.iter().sum::<i32>();
println!("{sum1} + {addend} = {sum2}");
assert_eq!(9, s.into_iter().sum::<i32>());
}
}
カーゴランの走行結果
Rust の Vec は順序付けられたコレクション メカニズムと一連の操作メソッドを提供するため、ここではコレクション コンテナ Vec がスタックの基礎となる実装として使用されます。他の実装を行うには、Vec のどちらの端がスタックの最上位になるかを選択するだけで済みます。オペレーション。次のスタック実装では、Vec の末尾がスタックの最上位要素を保持し、スタックが成長するにつれて、新しい項目が Vec の末尾に追加されることを前提としています。挿入されたデータの型が不明なため、汎用データ型 T が使用されます。さらに、反復関数を実現するために、IntoIter、Iter、IterMut の 3 つの構造体がここに追加され、それぞれ 3 つの反復関数が完成します。
アプリケーション: 括弧のマッチング、加算、減算、乗算および除算の優先順位のマッチング
// par_checker3.rs
fn par_checker3(par: &str) -> bool {
let mut char_list = Vec::new();
for c in par.chars() {
char_list.push(c); }
let mut index = 0;
let mut balance = true;
let mut stack = Stack::new();
while index < char_list.len() && balance {
let c = char_list[index];
// 将开始符号入栈
if '(' == c || '[' == c || '{' == c {
stack.push(c);
}
// 如果是结束符号,则判断是否平衡
if ')' == c || ']' == c || '}' == c {
if stack.is_empty() {
balance = false;
} else {
let top = stack.pop().unwrap();
if !par_match(top, c) {
balance = false; }
}
}
// 非括号字符直接跳过
index += 1;
}
balance && stack.is_empty()
}
fn main() {
let sa = "(2+3){func}[abc]"; let sb = "(2+3)*(3-1";
let res1 = par_checker3(sa); let res2 = par_checker3(sb);
println!("sa balanced:{res1}, sb balanced:{res2}");
// (2+3){func}[abc] balanced:true, (2+3)*(3-1 balanced:false
}